TypeScript——兼具类型安全与轻便开发的灵魂语言

做者:DoubleJan

当咱们谈到TypeScript时,咱们究竟在谈什么?TypeScript与JavaScript到底是什么关系?TypeScript类型安全吗?TypeScript类型那么多,看不过来吗?看了这篇文章,这些问题都能弄明白!javascript

一 TypeScript基本介绍

1. TypeScript类型

TypeScript是JavaScript的超集,是带类型的JavaScript。css

TypeScript经过类型注解,来约束一个变量的类型。但类型注解是TypeScript层面的概念,被编译后的JavaScript代码不会存在“类型”,而且,类型错误不影响TypeScript编译过程。能够说,TypeScript的类型系统,仅仅在TypeScript环境中,给使用者类型提示或类型警告。java

2. TypeScript安装和编译

能够经过npm来安装TypeScriptgit

// 全局安装typescript模块

npm install -g typescript

// typescript使用tsc命令编译文件或进行其余操做

tsc demo.ts
复制代码

3. TypeScript类型的特性(深刻理解TypeScript类型的用意)

(1)开发时(TS层)的类型约束。TS层的类型和报错不影响编译过程,不会存在于编译后的JavaScript代码中。 (2)基于结构类型的类型兼容性和类型推断(区别于Java等语言的名义类型),只要两个类型之间转换时,不影响后续运算,那么它们就是类型兼容的。 (3)结合编译后的JavaScript代码来学习TypeScript的特性,能够更好的理解TypeScript。typescript

二 基础类型与变量声明

1. TypeScript支持的类型

TypeScript的基础类型创建在JavaScript之上,兼容JavaScript的全部类型,包括布尔值(boolean),数字(number),字符串(string),数组([]),null(null),undefined(undefined),。npm

除此以外,TypeScript还额外支持一些类型,包括枚举,元组,any,never,void。这不是说TypeScript包含非JavaScript的部分,实际上这些类型最终都被编译为JavaScript代码,使用JavaScript方式实现。数组

JavaScript提供了void运算符,它是一元运算符,用来对表达式进行求值,返回undefined。TypeScript不只支持void做为操做符,也容许void做为类型注解,表示空值。安全

2. 类型注解

除了元组,枚举这两种有复合或组合概念的类型之外,别的基本类型都经过{{:typename'}}的语法来进行类型注解。bash

let num: number;
let str: string;
let bool: boolean;
let strArr: string[];         // 数组的类型注解,只须要在任意类型后加上[]便可
let numArr: Array<number>;    // 也可使用数组泛型Array<type>
let nullValue: null;
let undefinedValue: undefined;
let anyValue: any;
let voidValue: void;
let neverValue: never;

// 元组的类型注解
let tuple: [string, number];

// 枚举的类型注解
enum Color { Red, Blue };
let color: Color;
复制代码

3. 元组类型

元组类型用来表示一个已知数量和类型的数组。各个元素的类型没必要相同,但建立变量后,对应索引的元素类型必须一一对应,数组元素不能多,不能少。dom

4. 解构赋值

JavaScript中,解构赋值时容许对变量重命名,使用的是 {{val: tmp}}的形式,这与TypeScript的类型注解语法冲突了。所以,在解构赋值出现变量重命名时,首先知足JavaScript的语义要求。以后,能够在解构的{{结构}}后面加上类型注解。

const obj = { x: 1, y: 2 }

const { x, y } = obj;
const { x: XX, y: YY }: { x: number, y: number } = obj;

console.log(x, y, XX, YY);  // 1 2 1 2
复制代码

其实这里的类型注解不是必须的,由于只要在TypeScript环境中定义的变量,一定要求是有类型的,也就是说,obj在作解构赋值时,内部属性的类型已经明确了。此时,TypeScript能够自行对类型作类型推断。同理,在定义obj时,使用字面量语法,也是不须要显式写出类型注解的。

5. 展开

展开运算容许后续对象属性覆盖前面已经定义的属性,但若是类型不一致,会产生报错。

const defaults = { a: 'a', b: 'b' }
let search = { a: 'a', b: 2 }
search = { ...search, ...defaults }
// TypeScript error: Type '{ a: string; b: string; }' is not assignable to type '{ a: string; b: number; }'
复制代码

6. any的赋值

any类型能够接受任何类型值的赋值,也能够接受了别的类型值后,赋值给不一样类型。此时被赋值的变量类型相应变化。

any类型基本上等于关闭了TypeScript的类型检查系统,应当少用。

7. 声明

TypeScript中的声明语句会建立三个实体中的一种:命名空间,类型或值。只有值是在JavaScript中可以实际看到的。声明命名空间的语句会建立一个命名空间,声明类型的语句会使用声明的模型建立一个类型并绑定到给定的名字上,只有声明一个值,才会输出在JavaScript代码中。

三 枚举类型

1. 默认枚举(数值枚举)

TypeScript经过{{enum}}关键字声明一个枚举类型,默认状况下,值是数字类型。

enum CardSuit { Clubs = 1, Diamonds, Hearts, Spades }

// {1: "Clubs", 2: "Diamonds", 3: "Hearts", 4: "Spades", Clubs: 1, Diamonds: 2, Hearts: 3, Spades: 4}
复制代码

枚举类型可让一组数据有一个跟友好的名称。枚举类型其实是由对象实现的。编译成JavaScript代码后,这个对象的属性名和属性值互为键值对,即一组属性名对应一组属性值,同时这些属性值也会是另外一组属性的属性名。

EnumObject[(EnumObject['key0'] = 0)] = 'key0';
EnumObject[(EnumObject['key1'] = 1)] = 'key1';
复制代码

默认状况下,用户定义的第一个枚举属性值为0,以后递增,也能够显式地给第一个枚举属性定义数字值,后面的值会据此递增。

注意事项: 能够,也只能给第一个枚举属性显式定义初始值。 通常能够从1开始定义,避免0带来的各类问题。 2. 非数值枚举 TypeScript容许枚举值的类型是其余类型,好比说字符串。但若是使用非数字类型,如字符串,来定义枚举,就必须把全部属性都提供初始值。

enum CardSuit { Clubs = 'sd', Diamonds, Hearts, Spades }
// SyntaxError: Enum member must have initializer
复制代码
  1. 常量枚举 使用const关键字,能够把枚举定义成常量的。此时,为了提高性能,TypeScript会将用到这个枚举的地方直接初始化为值,而不是枚举对象引用,在运行时,这个常量枚举对象不存在。

--preserveConstEnums编译选项可让编译器在编译时保留常量枚举定义,从而使常量枚举对象运行时存在。

四 类

1. 类型约束

Typescript支持对类的属性和方法的类型约束。类的类型约束上,被赋值为实例对象的变量,容许类型注解为对象的父类,调用内部方法时,依然会调用当前对象的类的方法,不会直接调用父类同名方法。

class Animal {
  name: string;
  private id: string;
  protected code: string;

  constructor(animalName: string) {
    this.name = animalName;
    this.id = (Math.random() * 100000 / 100000).toString();
    this.code = `CODE${(Math.random() * 100000 / 100000)}`;
  }

  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}`);
  }
}

class Snake extends Animal {
  constructor(animalName: string) {
    super(animalName);
  }

  move(distanceInMeters: number = 0) {
    console.log('From Snake');
    super.move(distanceInMeters);
  }
}

const snake: Animal = new Snake('snake');
snake.move(30);    // From Snake   snake moved 30
复制代码

在Javascript中,类的属性默认是public,可显式指定为private,protected

2. 抽象类

ES6没有提供抽象类,可是Typescript提供了抽象类。抽象类使用{{abstract}}关键字定义,抽象类的抽象方法不包含具体实现,且必须在派生类中实现。

abstract class Animal {
  abstract born(): void;
}
复制代码

3. 类类型

当咱们定义了一个类的时候,得到的是类的实例类型以及类静态资源。静态资源包括构造函数和方法。类除了实例类型,还存在类类型,即构造函数的类型。构造函数内,包含了类的静态属性。使用类型注解{{: classname}},获取的是实例类型,而若是想要获取类类型(构造函数类型),须要使用{{typeof classname}}。

const greeter1: Greeter = new Greeter();
// Greeting: HELLO
console.log(greeter1.greet());

const greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = 'HELLO FROM MAKER';

const greeter2: Greeter = new greeterMaker();
// Greeting: HELLO FROM MAKER
console.log(greeter2.greet());

// object function HELLO FROM MAKER
console.log(typeof greeter1, typeof greeterMaker, greeterMaker.standardGreeting);
复制代码

为何类的类型就是构造函数类型呢?能够在控制台打印出一个实例对象,在原型属性proto里面找到constructor,它就被标注为class

4. 用类作接口

类定义时会建立类的实例类型。即,类能够建立出类型。所以,在容许使用接口时,也容许使用类。

class Point {
  x: number;
  y: number;

  // 若是类的属性被定义却没有初始化,Typescript会报错
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

interface Point3D extends Point {
  z: number;
}

let point: Point3D = { x: 1, y: 2, z: 3 }
复制代码

详细的内容,在接口里面描述。

五 接口

1. 接口定义

TypeScript使用接口来对一个结构进行类型约束。接口的声明使用{{interface}}关键字。为接口的每个属性进行类型注解。在继承接口或使用接口做为变量类型时,接口中属性顺序不定,但类型必须按属性名一一对应,属性不能多,不能少。

interface Point {
  x: number;
  y: number;
}
const point: Point = { y: 10, x: 3.56 }
const point3D: Point = { x: 12.4, y: 5.09, y: 23 }   // TypeScript error: Duplicate identifier 'y'
复制代码

接口仅存在与TypeScript层,编译成JavaScript后,接口是不存在的。

2. 带只读属性的接口定义

接口定义时,可使用{{readonly}}关键字,来把属性修饰为只读的,即在对象建立时初始化值,后续不容许再修改。

interface Point {
  // ...
  readonly origin: number
}

class P {
  readonly origin: number

  constructor(o: number) {
    this.origin = o;
  }

  // TypeScript error: Cannot assign to 'origin' because it is a read-only property
  setOrigin(o: number) {
    this.origin = o;
  }
}
复制代码

readonly和const如何取舍?若是被注解的是变量,应当用const,若是被注解的是属性,应当用readonly

3. 带索引签名的接口定义

若是一个接口存在许多不肯定的额外属性,属性名,属性个数,都是不可预知的,可使用索引签名来声明这些属性。

interface Square {
  color?: string;
  width: number;
  [propName: string]: any
}

const sq: Square = { width: 100, 1: 'square', borderd: false }
复制代码

语句{{string: any}}意思是,容许接口接收除已经定义了的属性(在Square中是color和width)外,额外的任意属性,这些属性的键名的类型为string或number。接口内接收的全部属性类型为any,即任意一个类型的属性均可以接收。

其中,方括号内的propName没有实际意义,仅仅占位,换成{{string}}也能够。方括号内冒号后面表示属性名的类型,可选的值为string和number,由于在JavaScript中,属性名只能是字符串或数字类型(数字实际上也是被转化成字符串的)。选择string实际上也容许属性名类型为number。最后的any表示整个接口范围内,容许的全部的类型,这个类型注解也会约束前面已经定义了的属性类型。

4. 带函数的接口定义

接口中容许定义函数类型,只须要给函数的参数和返回值作类型注解便可。返回值为空时,类型注解为{{:void}},此时能够省略。

interface Point {
  printPoint(desc: string): void
}

// 或者是这个样子,这个时候只能实现为函数
interface P {
  (desc: string): void
}
const p: P = (desc: string) => {
  console.log(`P: ${desc}`);
}
复制代码

5. 类实现接口

普通类实现 TypeScript中,接口能够被类继承。类继承接口时,必须包含接口定义的全部变量和函数。须要注意的是,接口描述的内容要求类进行公有继承。若是把接口的变量继承为私有的会报错。

TypeScript和JavaScript中,类的属性和方法默认为公有的。

// 接口实现
interface ClockInterface {
  currentTime: Date
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  constructor(h: number, m: number) {}
  // private currentTime: Date = new Date();
  // Property 'currentTime' is private in type 'Clock' but not in type 'ClockInterface'
}
复制代码

类类型实现 一个类实现接口时,只对实例部分进行检查,所以,静态部分,好比构造函数是不会被检查的。若是接口要求实现构造函数,须要额外的写法。首先,构造函数的类型注解格式为:new (): 。

// 构造函数类型,即,类类型
interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  tick(): void;
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
  return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
  hour: number;
  minute: number;
  constructor(h: number, m: number) {this.hour = h; this.minute = m;}
  tick() {
    console.log(`it is ${this.hour}: ${this.minute} clock`);
  }
}

let digital = createClock(DigitalClock, 12, 25);

复制代码

类表达式 继承接口,或定义类,可使用类表达式语法,这样会简洁一些。

interface ClockConstructor {
  new(hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  tick(): void;
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) { }
  tick() { console.log('beep') }
}
复制代码

6. 继承与合并接口

接口继承 一个接口可使用{{extends}}继承其余接口。

interface Shape {
  color: string
}

interface Square extends Shape {
  sideLength: number;
}
复制代码

接口合并 能够重复声明同名接口,这时接口会合并,而且包含全部声明的部分。

interface Point {
  x: number;
  y: number;
  z?: number;
}
interface Point {
  // ...
  readonly origin: number
}
interface Point {
  printPoint(desc: string): void
}
复制代码

7. 接口继承类

接口也能够继承类。接口容许继承类的全部成员,但不包括实现(不管是属性的赋值仍是方法实现)。若是类具备静态部分和私有或受保护成员,那就必需要同时继承类,由于接口不检查静态,私有,和受保护部分

class Control {
  private state: any;
}

interface SelectableControl extends Control {
  select(): void
}

class Button extends Control implements SelectableControl {
  select() {}
}

// Property 'state' is missing in type 'Image' but required in type 'SelectableControl'
class Image implements SelectableControl {
  select() {}
}
复制代码

六 函数

1. 类型注解

Typescript要求函数提供类型注解。函数的类型注解包含两部分,参数和返回值的类型注解。一个完整的函数类型注解相似于这样:

let fun: (x: number, y: string) => string = function(x: number, y: string): string { return x + y }
复制代码

基于TypeScript的类型推断机制,若是使用这样的函数声明并赋值给变量,那么左右两侧的类型注解只写一侧便可。

当没有返回值时,类型为{{:void}},也能够省略不写。

interface Args {
  key: number
}

function returnArgs (arg: Args): Args {
  return arg;
}

function printArgs (arg: Args) {
  console.log('args: ', arg);
}
复制代码

2. 参数约束

Typescript中,函数的全部参数都是必须的。形参个数必须与实参数量一致,类型一致。若是没有值,可使用可选参数,写法为{{arg?: }},也能够给可选参数指定默认值。若是函数参数个数不定,可使用rest参数(剩余参数)。

须要注意的是,没有默认值的可选参数,rest参数,都是不定的,所以不能放在固定参数,有默认值的可选参数的前面,可选参数之间,不管是否有默认值,位置上不强制要求。rest参数必须放在全部参数的最后面。

interface Comples {
  (str: string, isPrint?: boolean, num?: number,  ...args: boolean[]): number
}

// args必须使用rest风格,即...开头,不然只会接收到剩余参数的第一个
const comples: Comples = (str, isprint = true, num, ...args) => {
  if (isprint) {
    console.log(`argumenst: ${str}, ${isprint}, ${num}, ${args}`)    
  }
  return str.length;
}

// argumenst: comples, true, 4, true,false,true
// 7
console.log(comples('comples', undefined, 4, true, false, true
复制代码

3. this类型约束

在JavaScript中,类型是没有约束的。对于this值,也只能本身跟踪。假设一个对象同时也表明着一种类型,那么假如在函数调用时出现了不一样于定义时的this对象类型,就能够显式的报错。Typescript容许在函数参数列表的第一个位置,显式指定this的类型,来约束this类型,也就_约等于_约束了this的指向。这只是一种约束形式,不会要求调用函数时,把this传递进来。

之因此是约等于,是由于本质上,Typescript约束的是类型,而不是实例。所以,一个同类型的不一样对象,Typescript不能在编译时检查出来。但我的感受同类型不一样实例的状况比较罕见。

// 使用bind绑定一个同类型的新对象
const res = d1.createCardPicker.bind({
  suits: ['hearts', 'spades'],
  cards: Array(52),
  createCardPicker: function (this: Deck) {
    return () => {
      const pickedCard = Math.floor(Math.random() * 52);
      return { suit: this.suits[3], card: pickedCard % 13 };
    }
  }
});

// res: {suit: undefined, card: 12}
console.log('res: ', res()());  
复制代码

4. 函数重载

TypeScript容许声明多个不一样参数的同名函数,并在最后一个函数声明后实现函数,这被称为函数重载。若是不一样的重载函数参数个数不一样,多出来的参数必须是可选的。

function printf(x: number, y: string): void; function printf(x: string, y: string, z?: boolean): void; function printf(x: any, y: any) {
  console.log('x, y', x, y);
}

export default () => {
  printf(1, 'yi');
  printf('er', '二');
  return null;
}
复制代码

函数重载只在TypeScript层出现,编译后的JavaScript代码不存在函数重载,所以不会形成运行时性能损耗

exports.__esModule = true;
function printf(x, y) {
    console.log('x, y', x, y);
}
exports["default"] = (function () {
    printf(1, 'yi');
    printf('er', '二');
    return null;
});
复制代码

5. undefined的函数传参

any类型兼容全部类型,也包括null,undefined,和any自己。可是,在函数传参时,参数缺失不等于传递{{undefined}}类型

const anyArch = (person: any) => {
  if (person == null) {
    people.push({
      Name: { firstName: 'AA', lastName: 'BB' },
      code: 12,
      isBad: true,
      area: ['guangdong', 'guangzhou', 'tianhe']
    });
  }
}

anyArch(undefined);  // success
anyArch();           // error: Expected 1 arguments, but got 0

复制代码

七 泛型

1. 泛型变量

有时候咱们须要让组件或函数,不只可以支持当前的数据类型,还但愿可以支持其余更多的类型,使用any能够勉强解决这个需求,可是类型检查就不存在了。TypeScript提供了相似于Java等语言的泛型支持。使用泛型变量来支持泛型的类型约束。

function printf<T>(arg: T):T {
  console.log('arg<type>: ', arg, typeof T);
  return arg;
}
复制代码

注意事项: 须要注意的是,{{T}}或者说类型变量,不能做为实际变量使用,例如:

typeof T  // 'T' only refers to a type, but is being used as a value here
复制代码

使用泛型时,可使用{{}}的方式显式声明类型。也能够借助TypeScript的类型推断能力。

printf<number>(34);
printf(34)   // 类型推断
复制代码

2. 泛型变量数组

泛型也容许使用类型变量数组。声明类型变量写法不变,只要使用到的时候在类型变量后面加上一对方括号,或使用Array泛型便可,例如{{T[]}},Array

function printList<T>(arg: T[]):T[] {
  console.log('arg<type>: ', arg);
  return arg
}
printList<number>([34, 56])
复制代码

3. 泛型接口

声明一个泛型接口,不须要对声明语句有特殊写法,只须要在内部根据须要使用泛型变量定义属性和方法便可。与声明普通的变量和方法没有什么区别

interface GenericIdentity {
    <T>(arg: T): string;
}
复制代码

4. 泛型类

泛型类写法与泛型接口差很少。泛型类使用{{<>}}括起泛型类型,跟在类名后面。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => string
}
const mGenericNumber = new GenericNumber<number>();
复制代码

5. 泛型约束

使用了泛型之后,不能随意使用泛型的属性,由于泛型的具体类型是不肯定的。为了保证某一属性一定存在,可让泛型变量继承一个接口。

interface Lengthwise {
    length: number
}
function loggingIdentity<T extends Lengthwise> (arg: T): string {
    return `arg<${typeof arg}>: ${JSON.stringify(arg)}`;
}
loggingIdentity([3, 7, 5]);
复制代码

6. 使用类型参数

若是须要从泛型类型中,拿到某一个属性的类型,可使用索引查询操做符{{keyof}}。经过{{keyof}}来获取一个类型的某一属性的类型。

// K extends keyof T 这里extends表示K的类型应该来源于T的某一属性类型
function printProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}
复制代码

7. 使用类类型的泛型参数约束

前面提到的使用{{extends}}关键字,约束左值从右值派生出来。不只能做用于属性和类之间,也能够在两个继承关系的类之间使用。从而约束一个类型参数,必须来源于另外一个类型参数。

class Animal {
    name: string;
    constructor(n: string) {
        this.name = n;
    }
}

class Cat extends Animal {
    keeper: CatKeeper
    constructor(name) {
        super(name);
        this.keeper = new CatKeeper();
        this.keeper.count = 1;
    }
}

function createInstance<A extends Animal>(obj: new (n: string) => A, name: string): A {
    return new obj(name);
}
复制代码

八 类型推断与类型兼容

1. 最佳通用类型

当须要从几个表达式中推断类型的时候,会使用这些表达式的类型来推断出一个最合适的通用类型。默认状况下,会检查全部的表达式:

检查它们是否有共同的类型,好比是否都有同一个父类。若是有,就会被推断为该类型 若是没有共同的类型,好比所有是基础类型,默认会使用联合类型,形如{{string | number}}。

2. 上下文归类

若是当前表达式所须要的类型,在以前已经定义过,或者根据程序逻辑,这里的类型是明确的,此时,TypeScript会采用上下文归类的原则,推断出这里的类型。

典型的,好比匿名函数赋值给一个变量,那么类型定义只须要写一遍,或者当一个函数返回一个数字,那么接收这个返回值的变量也是不用定义类型的。

const fn = (num: number, isNum: boolean): string => `${num} is number ?: ${isNum}`
复制代码

3. 结构类型

TypeScript的类型系统是基于结构类型的。结构类型区别于Java为表明的名义类型。在Java这种基于名义类型的语言中,即使两个类具备彻底相同的属性,只要是分别定义的不一样类,它们的实例就是不一样的类型。但TypeScript不一样,TypeScript的类型管理基于结构类型,只要两个结构体具备相同的属性,那么类型就是相同的。

对于结构类型的类型兼容来讲,一个结构x要想类型兼容另外一个结构y,至少y具备与x相同的属性。

interface Named {
    name: string
}

class Person {
    name: string
    constructor(name: string) {
        this.name = name;
    }
}
const p: Named = new Person('username');
`Person name<${typeof p.name}>: ${p.name}`;  // Person name<string>: username
复制代码

4. 函数的参数类型兼容

若是一个函数x的参数列表中,每个参数的位置和类型,都能在另外一个函数y的参数列表中一一对应(y中多余的参数不作限制),那么x就是对y参数类型兼容的。简单来讲就是,参数需求少的函数,容许在提供更多的参数的函数类型上使用。

典型的应用场景就是,Array的原型方法,map, forEach等等的回调函数中,它们被定义为接收三个参数,当前遍历的元素,元素索引,整个数组,但一般提供只接收前几个的参数的回调函数。

let x = (a: number, b: number) => a - b;
let y = (b: number, increment: number, c: string) => b + increment;

// x的全部参数都能在y里面找到
y = x;
复制代码

5. 函数的返回值类型兼容

一个函数x的返回值类型,若是是另外一个函数的返回值类型的子类型,即x的返回值对象的每个属性都能在y返回值中找到(y中多余的属性不作限制),那么x是对y返回值类型兼容的。简单来讲就是,提供返回值更少的兼容更多的。

let x = () => ({ name: 'Double' });
let y = () => ({ name: 'Float', location: 'Home' });

// x的返回值的属性,在y中全存在
x = y;
复制代码

6. 可选参数和剩余参数

可选参数和剩余参数不影响类型兼容的判断,容许目标函数的可选参数不在源函数的参数列表中,也容许源函数的可选参数不在源函数的参数列表中。剩余参数被看成是无限个可选参数。

function handler(args: number[], callback: (...args: number[]) => number ) {
    return callback(...args);
}
handler([2, 4, 6], (x, y) => x + y);
handler([1, 3, 5], (...args) => args[2] - args[0])}, ${handler([1, 0], (...args) => args[2] - args[0])
复制代码

7. 枚举类型的类型兼容

枚举类型与数字类型或字符串类型是兼容,这字符串仍是数字,这取决于使用者给枚举类型定义的值的类型。不一样枚举类型之间不兼容。

enum Status { Ready, Waiting }
enum Color { Red, Green, Blue }
enum Select { NoA = 'AAA', NoB = 'BBB' }

const status = Status.Ready;
const str: string = Select.NoA;
// `status ==? Color.Red ${status == Color.Red}` // TypeScript error

// const num: number = Select.NoA; // TypeScript error 
console.log(`status: ${status}, Select NoA: ${str}`);  // status: 0, Select NoA: AAA
复制代码

8. 类的兼容性

检查类的兼容性时,只考虑实例部分,静态成员和构造函数不影响类的类型兼容性。

class Animal {
    name: string;
    constructor(name: string, age: number) {
        this.name = name;
    }
}
class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
let a: Animal = new Animal('a animal', 5);
let p: Person = new Person('a person');
a = p;  // OK
复制代码

类的私有和保护成员会影响类型兼容性

class Animal {
    name: string;
    protected age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

class Person {
    name: string;
    protected age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

let a: Animal = new Animal('a animal', 5);
let p: Person = new Person('a person', 22);
// 私有成员来自不一样类,类型不兼容
 a = p;    // TypeScript error
复制代码

9. 泛型的类型兼容性

泛型的写法通常为{{TypeName}},T就是一个泛型变量。在使用泛型时,咱们一般是传入一个类型,返回一个泛型包装的结果类型。也就是说,真正影响到后续程序和运算的,是结果类型。所以,泛型的类型兼容性判断上,仅考虑结果类型,即使泛型名称,泛型变量不一样,只要结果类型一致,那么它们就是类型兼容的。

interface GenericInterface<T> {
    data: T
}

let x: GenericInterface<number>;
let y: GenericInterface<string>;
x = y;     // TypeScript error

interface EmptyInterface<T> { }

let a: EmptyInterface<number>
let b: EmptyInterface<string>

a = b;
a = 34;
复制代码

九 高级类型

1. 交叉类型

交叉类型写法相似于{{T & U}},用于将多个类型合并为一个类型。交叉类型要求同时知足全部指定的类型的要求。也就是全部类型的并集(包含全部属性)。若是函数的返回值是交叉类型,必须作显式类型转换(类型断言)。若是几个类型中有同名属性,后面的属性值会覆盖前面的属性值。

function extend<First, Second>(first: First, second: Second): First & Second {
    const result: First & Second = { ...first, ...second }
    return <First & Second>result; } class Cat { name: string; catchMouse: boolean; constructor(n, c) { this.name = n; this.catchMouse = c; } } class Dog { name: string; walkDog: boolean; constructor(n, w) { this.name = n; this.walkDog = w; } } const result = extend({ name: 'a cat', catchMouse: false }, { name: 'a dog', walkDog: true }); 复制代码

2. 联合类型

联合类型用于在一堆备选类型中,选择任意一个类型。它的出现是为了某些时候确实须要考虑不一样类型,但使用any又丢失了类型约束,所以使用联合类型,将容许的类型罗列出来。

function unionAnimal(animal): Cat | Dog {
    const result = { ...animal };
    return <Cat | Dog>result; } class Cat { name: string; catchMouse: boolean; constructor(n, c) { this.name = n; this.catchMouse = c; } } class Dog { name: string; walkDog: boolean; constructor(n, w) { this.name = n; this.walkDog = w; } } 复制代码

// 这不能用类型断言,强制转换成Cat类型 const result = unionAnimal({ name: 'a cat', catchMouse: false });

// 只能访问联合类型共有成员 // union: ${result.name}, ${result.walkDog}, ${result.catchMouse}; // TypeScript error console.log(union: ${JSON.stringify(result)});

## 3. 类型守卫与类型谓词
以上面的联合类型为例,通常状况下,只能访问联合类型的特有成员,而且不能用强制类型转换,将联合类型转换成某一备选类型。此时,就须要使用类型守卫和类型谓词,来确认联合类型的结果类型,一定是某一备选类型。

类型谓词使用{{is}}关键字,形如{{t is T}},来判断某一变量是不是某个类型。咱们能够定义一个函数,返回一个类型谓词,用来判断变量的类型,这个函数就是一个类型守卫。
```javascript
class Cat {
    name: string;
    catchMouse: boolean;
    constructor(n, c) {
        this.name = n;
        this.catchMouse = c;
    }
}

class Dog {
    name: string;
    walkDog: boolean;
    constructor(n, w) {
        this.name = n;
        this.walkDog = w;
    }
}

// 自定义的类型守卫,返回一个类型谓词
function isCat(animal: Cat | Dog): animal is Cat {
    return (<Cat>animal).catchMouse !== undefined;
}

// 若是这里定死了Cat,TypeScript会知道,永远进不去else分支,so就不能使用walkDog
// 此时else分支被认为是never的: Property 'walkDog' does not exist on type 'never'
const result = Math.random() < 0.5 ? new Cat('a cat', false) : new Dog('a dog', true);

// TypeScript不只知道在if分支里是Cat类型; 它还清楚在else分支里,必定不是Cat类型,必定是Dog类型
if (isCat(result)) {
    console.log(`result is ${JSON.stringify(result)}`);
} else {
    console.log(`another result ${result.walkDog ? 'can' : 'can\'t'} walk the dog`);
}
复制代码

4. typeof类型守卫

当typeof操做符是按如下两种方式被使用的时候:

typeof v === 'typename'
typeof v !== 'typename'
typeof被当作是一个类型守卫,不须要提供函数实现,直接使用便可。此时,typename必须是{{number}}, string, boolean, {{symbol}}的其中一种。typeof与其余类型或字符串也能够比较,此时不会被当作类型守卫。

// 能够将typeof类型守卫定义为一个函数
function isNumber(x: any): x is number {
    return typeof x === 'number';
}
// 直接使用typeof,会被默认为类型守卫
console.log(`'sdd' is Number: ${isNumber('sdd')}, 'sdd' is string: ${typeof 'sdd' === 'string'}`);
复制代码

5. instanceof类型守卫

在JavaScript中,instanceof操做符的做用是,检测一个构造函数的原型是否存在于某一个实例对象的原型链上。在TypeScript中,instanceof依然是这个做用,而且,它被默认的看成是类型守卫。

class Animal {
    name: string;
    constructor(n) { this.name = n; }
}

class Pet extends Animal {
    constructor(n) { super(n); }
}

class Dog extends Pet {
    walkDog: boolean;
    constructor(n, w) {
        super(n);
        this.name = n;
        this.walkDog = w;
    }
}

const pet = new Pet('a pet') instanceof Animal;
const dog = new Dog('a dog', false) instanceof Animal;

console.log(`pet is animal: ${pet}, dog is Animal: ${dog}`);
复制代码

6. 类型别名

类型别名使用{{type}}关键字来声明,顾名思义,它是给一个类型或类型表达式定义一个别名。类型别名的使用时机,能够参考如下几点:

基础类型没有必要使用类型别名,没有意义。 类型别名与接口是有区别的。类型别名不会建立一个真实的名字,只是建立一个引用。接口定义并建立了一个名字,而且接口能够被继承,而类型别名不能够。 当使用多个类型的联合类型或交叉类型时,应当定义一个类型别名,提高程序可读性。 类型别名仅能用于类型注解,不能当作变量使用。

type Name = string;
type GetName = <T>(param: T) => string;
type Age = number;
type Info = Name & Age;  // 一个变量不可能同时是字符串和数字类型,所以这行建立的Info的别名为never类型
type OneOf = Name | Age;
const getName: GetName = function <T>(param: T): string {
    return JSON.stringify(param);
}
const name: Name = 'a name';
const age: Age = 25;
const info: Info = 25;    // TypeScript error
const oneof: OneOf = 11;

console.log(`${name}'s age is ${age} or ${oneof}, is never: ${getName({ never: true })}`);
复制代码

7. 数字和字符串字面量类型

字符串和数字的字面量类型,用于给变量规定字符串或数字的可选值的范围。

type AB = 'A' | 'B';
type BIN = 1 | 0;
const ab: AB = 'C';    // TypeScript error
const bin = 1;

console.log(`BIN: ${bin}`);
复制代码

8. 索引类型

若是一个变量是另外一个变量的一个属性,能够经过索引类型查询操做符{{keyof}}和索引访问操做符{{[]}}进行类型注解和访问。

// T是一个任意类型,K类型是T类型中,任意一个属性的类型,形参names是K类型变量组成的数组
// 返回值 T[K][]: T类型的K属性数组(第一个方括号表示取属性,第二个表示数组类型)
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
    return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}

const person: Person = {
    name: 'doublejan',
    age: 17
}

const strs: string[] = pluck(person, ['name']);
console.log(`Person Name: ${strs}`);
复制代码

9. 映射类型

(1) 同态映射 对于一些属性,咱们但愿它们可以有公共的约束,好比,一个对象的一些属性是可选的,或是只读的。固然能够一个个的设置,可是更优雅的方式是使用映射类型,将旧类型用相同的方式转换出来一批新类型。

interface Person {
    name: string;
    age: number;
}

// 这里使用了索引查询操做符 keyof 把P变量的类型绑定为T的属性类型
// 又使用索引签名的语法 [prop: propType]: <type>,匹配到传进来的泛型T的全部属性
// 这种映射被称为 同态映射 ,由于全部的映射都是发生在类型T之上的,没有别的变量和属性参与
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

const pPartial: Partial<Person> = { name: 'only name' }
const pReadonly: Readonly<Person> = { name: 'const name', age: 32 }
复制代码

(2) 层叠映射 映射就像css同样,是能够层叠的,编译器在声明新的类型前,会拷贝已经存在的全部类型修饰符。假如某一类型的全部属性被映射为可选的,此时再通过只读映射包装,那么全部的类型就是可选且只读的。

10. 条件类型

普通的有条件类型 有条件的类型会以一个条件表达式进行类型关系检测,从而在两个类型中任选其一。条件类型的写法相似于{{T extends U ? X : Y}}或者解析为x,或者解析为y,再或者延迟解析。条件类型不能转换成任意一个备选的类型。

// return `${f(Math.random() < 0.5)}`;
// TypeName<T>是一个条件类型,用于检测T的类型,返回一个类型明确的类型字面量
type TypeName<T> =
    T extends string ? string :
    T extends number ? number :
    T extends boolean ? boolean :
    T extends undefined ? undefined :
    T extends () => string ? () => string :
    T extends Function ? () => void :
    object;

// 如下type关键字定义的类型,通过TypeName<T>的条件类型检测,由返回的类型,生成对应的类型别名.
type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"

type T3 = TypeName<() => string>;  // "function"
type T4 = TypeName<() => void>;
type T5 = TypeName<string[]>;  // "object"

const fn: T3 = () => 'T3 is Function Type';
const fnVoid: T4 = function () { }
const str: T0 = 'string type';

console.log(`${fn()}, ${fnVoid()}, ${str}`);
分布式条件类型
分布式有条件类型在实例化时会自动分发为联合类型。例如,实例化{{T extends U ? X : Y}},T的类型为{{A | B | C}},会被解析为{{(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)}}

type TypeName<T> =
    T extends string ? string :
    T extends number ? number :
    T extends boolean ? boolean :
    T extends undefined ? undefined :
    T extends () => string ? () => string :
    T extends Function ? () => void :
    object;

type T12 = TypeName<string | string[] | undefined>;

const obj: T12 = { key: 'value' }
console.log(`obj: ${JSON.stringify(obj)}`);
复制代码
相关文章
相关标签/搜索