TypeScript:最佳实践

December 12, 2019

类型保护

typescript 中有三种正常的类型保护,包括自定义类型保护,typeof 类型保护和 instanceof 类型保护。其中 typeof 只能用于 javascript 定义的六种基础类型,而 instanceof 只能用于当类型在要检查类型的原型链中。对于其他 typescript 中其他丰富的类型,typeof 和 instanceof 都无能为力。

自定义类型保护

那么只剩下自定义保护能使用了,在官方文档中使用如下例子讲解自定义保护。

function isFish(pet: Fish | Bird): pet is Fish {
  return (<Fish>pet).swim !== undefined
}

但是有很多问题。第一,它使用了违反直觉的类型断言,因为不断言,就访问不到属性.swim。第二,如果 Fish 没有特例属性,则无法将它和其他类型区分。考虑以下例子

interface Cash {}
interface PayPal {
  email: string
}
interface CreditCard {
  cardNumber: string
  securityCode: string
}

type PaymentMethod = Cash | PayPal | CreditCard

function StringifyPaymentMethod(menthod: PaymentMethod): string {}

函数要在内部区分各种支付方式,然后使用不同的处理方法。

在这里普通的类型保护就起不了作用了。

应用字面字符串类型,则可很好解决这个问题。

interface Cash{ kind: 'Cash' };
interface PayPal{ kind: 'PayPal'; email: string; };
interface CreditCard{ kind: 'CreditCard'; cardNumber: string; securityCode: string };

type PaymentMethod = Cash | PayPal | CreditCard;

function StringifyPaymentMethod(menthod: PaymentMethod): string {
  switch(mentod){
    case 'Cash': ...
    case 'PayPal': ...
    case 'CreditCard': ...
  }
};

这里的好处有两点:一是不用再使用类型断言,二是不再依赖类型特定属性(其实说到底就是加了一个特例属性嘛)。但是即便是不使用类型断言,类型还是被保护了,在 case ‘Cash’内部,Method 则会被直接当做 Cash 类型。

依靠这种自定义的类型保护,可以更好的判断包括 interface,泛型等非标准类型。

索引类型

使用索引类型,编译器就能够检查使用了动态属性名的代码

考虑以下例子:

interface Type {
  a: number
  b: string
  c: string[]
}

function prop(obj: Type, key: "a" | "b" | "c") {
  return obj[key]
}

可以预料到,function prop 返回类型将是 number | string | string[],因为你无法预料输入参数是 a,b,还是 c,因为是动态的。但编译器知道只能是这三个值,所以返回值只能是 number,string 或 string[]。

但这似乎不是我们想要的,我们希望编译器在编译的时候就知道将会返回值的类型。

typescript 拥有关键字 keyof,用以指代一个类型的所有 key 的值的集合。

在这个例子中可以将

function prop(obj: Type, key: "a" | "b" | "c")

改成

function prop(obj: Type, key keyof Type)

cool, 但是状况似乎没什么变化。

这里就要引入到 typescript 中的索引类型。在 typescript 中指定索引 T[K],则会被解释成类型。例如这里

function prop(obj: Type, key): Type[a] {}

则会被解析成 number。利用这个特性,可以解决这个问题

interface Type {
  a: number
  b: string
  c: string[]
}

function prop<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]
}

在这个例子中,返回值会被隐式解释成 T[K],换言之,返回值直接与参数 key 和 obj 关联了,现在它可以正确指示返回值。

这种模式也被广泛应用于 typescript 的一些装饰性的类型,例如考虑以下例子:

interface Point {
  x: number
  y: number
}

type ReadOnlyPoint = ReadOnly<Point>

// ReadOnlyPoint {
//   readonly x: number;
//   readonly y: number;
// }

ReadOnly 的定义

type ReadOnly<T> = {
  readonly [P keyof T]: T[P];
}

在这个例子中,利用了泛型 ReadOnly 装饰了接口 Point,使之变成了一个只读的类型。这种模式也同样可以应用于 nullable,optional 等装饰性的类型上

type Nullable<T> = {
  [P keyof T]: T[P] | null;
}

type Optional<T> = {
  [P keyof T]?: T[P];
}

甚至还可以形成装饰链

type CustomPoint = ReadOnly<Nullable<Optional<Point>>>

// CustomPoint {
//   readonly x?: number | null | undefined;
//   readonly y?: number | null | undefined;
// }

Written by 梁伯豪