今天看啥  ›  专栏  ›  影子科技前端团队

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

影子科技前端团队  · 掘金  ·  · 2020-01-02 10:12
阅读 18

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

作者:DoubleJan

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

一 TypeScript基本介绍

1. TypeScript类型

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

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

2. TypeScript安装和编译

可以通过npm来安装TypeScript

// 全局安装typescript模块

npm install -g typescript

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

tsc demo.ts
复制代码

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

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

二 基础类型与变量声明

1. TypeScript支持的类型

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

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

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

2. 类型注解

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

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. 元组类型

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

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)}`);
复制代码



原文地址:访问原文地址
快照地址: 访问文章快照