阅读 7982

TypeScript 强大的类型别名

TS 有个非常好用的功能就是类型别名。

类型别名会给一个类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

一些关键字

使用类型别名可以实现很多复杂的类型,很多复杂的类型别名都需要借助关键字,我们先来了解一下几个常用的关键字:

extends

extends 可以用来继承一个类,也可以用来继承一个 interface,但还可以用来判断有条件类型:

T extends U ? X : Y;
复制代码

上面的类型意思是,若 T 能够赋值给 U,那么类型是 X,否则为 Y

原理是令 T'U' 分别为 TU 的实例,并将所有类型参数替换为 any,如果 T' 能赋值给 U',则将有条件的类型解析成 X,否则为Y

上面的官方解释有点绕,下面举个栗子:

type Words = 'a'|'b'|"c";

type W<T> = T extends Words ? true : false;

type WA = W<'a'>; // -> true
type WD = W<'d'>; // -> false
复制代码

a 可以赋值给 Words 类型,所以 WAtrue,而 d 不能赋值给 Words 类型,所以 WDfalse

typeof

在 JS 中 typeof 可以判断一个变量的基础数据类型,在 TS 中,它还有一个作用,就是获取一个变量的声明类型,如果不存在,则获取该类型的推论类型。

举两个栗子:

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

const jack: Person = { name: 'jack', age: 100 };
type Jack = typeof jack; // -> Person

function foo(x: number): Array<number> {
  return [x];
}

type F = typeof foo; // -> (x: number) => number[]
复制代码

Jack 这个类型别名实际上就是 jack 的类型 Person,而 F 的类型就是 TS 自己推导出来的 foo 的类型 (x: number) => number[]

keyof

keyof 可以用来取得一个对象接口的所有 key 值:

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

type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[];  // "length" | "push" | "pop" | "concat" | ...
type K3 = keyof { [x: string]: Person };  // string | number
复制代码

in

in 可以遍历枚举类型:

type Keys = "a" | "b"
type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any }
复制代码

上面 in 遍历 Keys,并为每个值赋予 any 类型。

infer

在条件类型语句中, 可以用 infer 声明一个类型变量并且对它进行使用,

我们可以用它获取函数的返回类型, 源码如下:

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

其实这里的 infer R 就是声明一个变量来承载传入函数签名的返回值类型, 简单说就是用它取到函数返回值的类型方便之后使用。

内置类型别名

下面我们看一下 TS 内置的一些类型别名:

Partial

Partial 的作用就是可以将某个类型里的属性全部变为可选项 ?

源码:

// node_modules/typescript/lib/lib.es5.d.ts

type Partial<T> = {
    [P in keyof T]?: T[P];
};
复制代码

从源码可以看到 keyof T 拿到 T 所有属性名, 然后 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值. 结合中间的 ?,将所有属性变为可选.

Required

Required 的作用刚好跟 Partial 相反,Partial 是将所有属性改成可选项,Required 则是将所有类型改成必选项,源码如下:

// node_modules/typescript/lib/lib.es5.d.ts

type Required<T> = {
    [P in keyof T]-?: T[P];
};
复制代码

其中 -? 是代表移除 ? 这个 modifier 的标识。

与之对应的还有个 +? , 这个含义自然与 -? 之前相反, 它是用来把属性变成可选项的,+ 可省略,见 Partial

再拓展一下,除了可以应用于 ? 这个 modifiers ,还有应用在 readonly ,比如 Readonly.

Readonly

这个类型的作用是将传入的属性变为只读选项。

// node_modules/typescript/lib/lib.es5.d.ts

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
复制代码

给子属性添加 readonly 的标识,如果将上面的 readonly 改成 -readonly, 就是移除子属性的 readonly 标识。

Pick

这个类型则可以将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

源码实现如下:

// node_modules/typescript/lib/lib.es5.d.ts

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
复制代码

从源码可以看到 K 必须是 T 的 key,然后用 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值。

Record

该类型可以将 K 中所有的属性的值转化为 T 类型,源码实现如下:

// node_modules/typescript/lib/lib.es5.d.ts

type Record<K extends keyof any, T> = {
    [P in K]: T;
};
复制代码

可以根据 K 中的所有可能值来设置 key,以及 value 的类型,举个例子:

type T11 = Record<'a' | 'b' | 'c', Person>; // -> { a: Person; b: Person; c: Person; }
复制代码

Exclude

Exclude 将某个类型中属于另一个的类型移除掉。

源码的实现:

// node_modules/typescript/lib/lib.es5.d.ts

type Exclude<T, U> = T extends U ? never : T;
复制代码

以上语句的意思就是 如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T,最终结果是将 T 中的某些属于 U 的类型移除掉,举个例子:

type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>;  // -> 'b' | 'd'
复制代码

可以看到 T'a' | 'b' | 'c' | 'd' ,然后 U'a' | 'c' | 'f' ,返回的新类型就可以将 U 中的类型给移除掉,也就是 'b' | 'd' 了。

Extract

Extract 的作用是提取出 T 包含在 U 中的元素,换种更加贴近语义的说法就是从 T 中提取出 U,源码如下:

// node_modules/typescript/lib/lib.es5.d.ts

type Extract<T, U> = T extends U ? T : never;
复制代码

以上语句的意思就是 如果 T 能赋值给 U 类型的话,那么就会返回 T 类型,否则返回 never,最终结果是将 TU 中共有的属性提取出来,举个例子:

type T01 = Extract<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>;  // -> 'a' | 'c'
复制代码

可以看到 T'a' | 'b' | 'c' | 'd' ,然后 U'a' | 'c' | 'f' ,返回的新类型就可以将 TU 中共有的属性提取出来,也就是 'a' | 'c' 了。

ReturnType

该类型的作用是获取函数的返回类型。

源码的实现

// node_modules/typescript/lib/lib.es5.d.ts

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

实际使用的话,就可以通过 ReturnType 拿到函数的返回类型,如下的示例:

function foo(x: number): Array<number> {
  return [x];
}

type fn = ReturnType<typeof foo>; // -> number[]
复制代码

ThisType

这个类型是用于指定上下文对象类型的。

// node_modules/typescript/lib/lib.es5.d.ts

interface ThisType<T> { }
复制代码

可以看到声明中只有一个接口,没有任何的实现,说明这个类型是在 TS 源码层面支持的,而不是通过类型变换。

这类型怎么用呢,举个例子:

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

const obj: ThisType<Person> = {
  dosth() {
    this.name // string
  }
}
复制代码

这样的话,就可以指定 obj 里的所有方法里的上下文对象改成 Person 这个类型了。

InstanceType

该类型的作用是获取构造函数类型的实例类型。

源码实现:

// node_modules/typescript/lib/lib.es5.d.ts

type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;
复制代码

看一下官方的例子:

class C {
    x = 0;
    y = 0;
}

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error
复制代码

NonNullable

这个类型可以用来过滤类型中的 nullundefined 类型。

源码实现:

// node_modules/typescript/lib/lib.es5.d.ts

type NonNullable<T> = T extends null | undefined ? never : T;
复制代码

比如:

type T22 = string | number | null;
type T23 = NonNullable<T22>; // -> string | number;
复制代码

Parameters

该类型可以获得函数的参数类型组成的元组类型。

源码实现:

// node_modules/typescript/lib/lib.es5.d.ts

type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;
复制代码

举个栗子:

function foo(x: number): Array<number> {
  return [x];
}

type P = Parameters<typeof foo>; // -> [number]
复制代码

此时 P 的真实类型就是 foo 的参数组成的元组类型 [number]

ConstructorParameters

该类型的作用是获得类的参数类型组成的元组类型,源码:

// node_modules/typescript/lib/lib.es5.d.ts

type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;
复制代码

举个栗子:

class Person {
  private firstName: string;
  private lastName: string;
  
  constructor(firstName: string, lastName: string) {
      this.firstName = firstName;
      this.lastName = lastName;
  }
}

type P = ConstructorParameters<typeof Person>; // -> [string, string]
复制代码

此时 P 就是 Personconstructor 的参数 firstNamelastName 的类型所组成的元组类型 [string, string]

自定义类型别名

下面是一些可能会经常用到,但是 TS 没有内置的一些类型别名:

Omit

有时候我们想要继承某个接口,但是又需要在新接口中将某个属性给 overwrite 掉,这时候通过 PickExclude 就可以组合出来 Omit,用来忽略对象某些属性功能:

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

// 使用
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }
复制代码

Mutable

将 T 的所有属性的 readonly 移除:

type Mutable<T> = {
  -readonly [P in keyof T]: T[P]
}
复制代码

PowerPartial

内置的 Partial 有个局限性,就是只支持处理第一层的属性,如果是嵌套多层的就没有效果了,不过可以如下自定义:

type PowerPartial<T> = {
    // 如果是 object,则递归类型
    [U in keyof T]?: T[U] extends object
      ? PowerPartial<T[U]>
      : T[U]
};
复制代码

Deferred

相同的属性名称,但使值是一个 Promise,而不是一个具体的值:

type Deferred<T> = {
    [P in keyof T]: Promise<T[P]>;
};
复制代码

Proxify

T 的属性添加代理

type Proxify<T> = {
    [P in keyof T]: { get(): T[P]; set(v: T[P]): void }
};
复制代码

如有疑问,欢迎斧正!

参考

TypeScript中文网

TS 中的内置类型简述

TypeScript 一些你可能不知道的工具泛型的使用及其实现

关注下面的标签,发现更多相似文章
评论