Index

October 5, 2019

TS 学习总结:编译选项 && 类型相关技巧

编译器选项

合法的 JS 代码就是合法的 TS 代码,所以我们在使用 TS 的时候,在默认的编译器配置下,受到的约束比较少。为了不让 TypeScript 变成 AnyScript,我们需要了解 TS 的编译器选项,并进行合理的配置。TS 配置中有一个 --strict,开启了这个选项就等于开启了 --noImplicitAny, --noImplicitThis, --alwaysStrict, --strictBindCallApply, --strictNullChecks, --strictFunctionTypes and --strictPropertyInitialization.。下面就来解析一下这些选项的作用:

--noImplicitAny

TS Deep Dive 中的相关说明。这个选项不是说不能使用 any,是不能使用隐式的 any。比如一个函数的入参,如果没有进行类型标注,那这个类型就是 any。这是隐式的。这个选项要求你把所有需要标注类型的地方都加上类型。当然你也可以在这些地方都使用 any,那就是另一回事了。可以使用 Linter 来禁止对显示 any 的使用。但有时候 any 也是有用的,比如这篇文章里提到的。不管如何,在纯 TS 项目里,noImplicitAny 还是有必要开启的。

--noImplicitThis

如果推断出 this 的类型是 any,就会报错。这样就可以避免一些因为 this 指向不正确而造成的问题。具体可以看这里

--strictBindCallApply

这个选项会在 bind,call,和 apply 时,根据原函数的类型签名对调用进行类型校验。是在 TS 3.2 版本中加入的。

function foo(a: number, b: string): string {
    return a + b;
}

let a = foo.apply(undefined, [10]);              // error: too few argumnts
let b = foo.apply(undefined, [10, 20]);          // error: 2nd argument is a number
let c = foo.apply(undefined, [10, "hello", 30]); // error: too many arguments
let d = foo.apply(undefined, [10, "hello"]);     // okay! returns a string

这个检查在泛型函数和有重载的函数有一些限制

--strictNullChecks

开启了这个选项之后,null,undefined 两个类型会被区分开来。TT | undefined 也会是不同的类型。一个类型必须显式说明有可能为 undefined。这个检查使得一些 undefined 错误可以被检查出来。具体例子参考 TS Deep Dive 的相关章节

类型相关技巧

下面总结一下 TS 中的类型相关的一些比较高级的内容

高级类型

TS 官方手册中的 Advanced Types 章节中提到了一些高级类型。通过合理的使用这些高级类型,可以让我们的类型检查和推断更完善和便捷。

Mapped types

Mapped types 指的是把一个类型的属性进行映射产出的新类型。

Partial

Partial 会把一个类型中所有的属性都变成 Optional,在业务中一些类型在使用的时候,不一定所有属性都有值,这个时候 Partial 很常用。

const mergeOptions = (options: Opt, patch: Partial<Opt>) {
  return { ...options, ...patch };
}

class MyComponent extends React.PureComponent<Props> {
  defaultProps: Partial<Props> = {};
}

Record<K,T>

业务中,我们经常会写枚举和对应的映射,Record 可以保证映射完整:

type AnimalType = 'cat' | 'dog' | 'frog';
interface AnimalDescription { name: string, icon: string }
const AnimalMap: Record<AnimalType, AnimalDescription> = {
  cat: { name: '猫', icon: '🐱'},
  dog: { name: '狗', icon: '🐶' },
  forg: { name: '蛙', icon: '🐸' }, // 拼写错误!
};

需要注意的是,Record 不是 homomorphic 的。

Readonly, Partial and Pick are homomorphic whereas Record is not. One clue that Record is not homomorphic is that it doesn’t take an input type to copy properties from:
Conditional Types

强烈推荐阅读 -> Conditional types in TypeScript - David Sheldrick 写的很通俗易懂,幽默风趣。

推荐阅读 -> Conditional Types in TypeScript - Marius Schulz 写的很详细,比官方文档总结的更好。

条件类型的用法是这样的:

T extends U ? X : Y

如果 T 和 U 兼容(T 包含 U 有的所有属性,T 可以被赋值给 U),这个类型就是 X,否则就是 Y。

看一下条件类型的实际用途:

比如有如下的函数,可能返回 string,也可能是 null:

function process(text: string | null): string | null {
  return text && text.replace(/f/g, "p")
}

但这样的类型写法是有问题的,因为返回值有可能是 null,没有 toUpperCase 这个方法。

//            ⌄ Type Error! :(
process("foo").toUpperCase()

这个时候我们可以用条件类型来解决:

function process<T extends string | null>(
  text: T
): T extends string ? string : null {
  ...
}

distributive conditional types

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

如果条件类型 extends 左边的参数是一个泛型类型变量的话,这个变量有是一个联合类型,那条件类型就是分布到联合类型的每一个成员上,返回一个新的联合类型。这有点类似 Map,只不过作用的是类型变量。

分布式条件类型和 never 类型一起用,可以起到一个筛选的效果。比如我们有一个条件类型叫 NonNullable:

type NonNullable<T> = T extends null | undefined ? never : T;

用起来就是这样的:

type T34 = NonNullable<string | number | undefined>;  // string | number
type T35 = NonNullable<string | string[] | null | undefined>;  // string | string[]

如果返回的类型中有 never,就会在联合类型中被丢弃,这样就产生了筛选的效果。

Type inference in conditional types

在条件类型中我们可以使用 infer 关键词来表示通过类型推导获得的类型。比如我们可以定义一个叫 ReturnType 的条件类型,用来获取函数的返回值类型:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

类似的还有 Parameters 类型:

type Parameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any
    ? P
    : never;
Index types

index type query and indexed access

keyof T 是 index type query,可以动态的返回类型的 key 组成的 union 类型。

The second operator is T[K], the indexed access operator. Here, the type syntax reflects the expression syntax. That means that person['name'] has the type Person['name']

indexed access operator 则是可以通过 indexed access 来得到类型 T 中某一个 key K 的 value 的类型。

综合运用例子,一个 getProperty 函数:

function getProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
    return o[propertyName]; // o[propertyName] is of type T[K]
}
Utility Types

上述的高级类型,在 TS 中已经内置的:

Interface VS Type Alias

interface 关键词和 type 关键词的区别:

Interface vs Type alias in TypeScript 2.7

官网文档中在 Advanced Types 中有一章讲 Interface VS Type Alias 的区别,但其中有一些已经过时了,并且还有一些区别没覆盖到。所以上面列出的博客,列出了在 2.7 版本中 interface 关键词和 type 关键词的区别:

上面的两点区别,union types 和 declaration merging,可以详细看上面的博文。