Fork me on GitHub

TypeScript 一些工具泛型的使用小结

在 TypeScript(下面统一简称 TS)中默认内置了很多工具泛型,能够合理灵活的使用这些工具,可以使我们的类型定义更加灵活,严谨。最近用 TS 重构了一版工程,下面是我根据自己的理解以及日常使用做的一些总结。

这些泛型定义在 node_modules/typescript/lib/lib.es5.d.ts 文件中,大概是从 1400 多行开始。所以,有兴趣也可以研究一下它们的源码实现,很有意思。

1. Partial

Partial 作用是将传入的属性变为可选项。

首先我们需要理解两个关键字 keyof 和 in, keyof 可以用来取得一个对象接口的所有 key 值。例如:

1
2
3
4
5
interface Foo {
name: string;
age: number
}
type T = keyof Foo // -> "name" | "age"

而 in 则可以遍历枚举类型, 例如:

1
2
3
4
type Keys = "a" | "b"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any }

keyof 产生联合类型, in 则可以遍历枚举类型, 所以他们经常一起使用

在 TS 中的源码实现:

1
2
3
type Partial<T> = {
[P in keyof T]?: T[P];
};

上面语句的意思是 keyof T 拿到 T 所有属性名, 然后 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值。结合中间的 ? 使属性可选。

2. Required

Required 作用是将传入的属性变为必选项。

在 TS 中的源码实现:

1
2
3
type Required<T> = {
[P in keyof T]-?: T[P];
};

我们发现一个有意思的用法 -?, 这里很好理解就是将可选项代表的 ? 去掉, 从而让这个类型变成必选项. 与之对应的还有个 +? , 这个含义自然与 -? 之前相反, 它是用来把属性变成可选项的。

3. Readonly

Readonly 作用是将传入的属性变为只读选项。

在 TS 中的源码实现:

1
2
3
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

4. Pick

Pick 作用是从 T 中将所有的 K 取出来,并生成一个新的类型。

在 TS 中的源码实现:

1
2
3
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

5. Record

Record 作用是将 K 中所有的属性的值转化为 T 类型。

在 TS 中的源码实现:

1
2
3
type Record<K extends keyof any, T> = {
[P in K]: T;
};

6. Exclude

Exclude 作用是从 T 中排除掉所有包含的 U 属性。

在 TS 中的源码实现:

1
type Exclude<T, U> = T extends U ? never : T;

7. Extract

Extract 作用正好和上面的 Exclude 相反。而是从 T 中提取出所有包含的 U 属性值。

在 TS 中的源码实现:

1
type Extract<T, U> = T extends U ? T : never;

8. Omit

Omit 作用是用来忽略 T 中的 K 属性。

在 TS 中的源码实现:

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

示例:

1
2
// 使用
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }

9. NonNullable

NonNullable 作用是去除 T 中包含的 null 或者 undefined。

在 TS 中的源码实现:

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

示例:

1
2
3
4
type TFoo = 1 | null | undefined

let foo: NonNullable<TFoo> = 1 // 正确
foo = null // 错误,因为这个值已经被去除

10. Parameters

Parameters 作用是用来获取一个函数的参数类型,而且返回的是只能包含一组类型的数组。

在 TS 中的源码实现:

1
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

示例:

1
2
3
4
5
6
type Func = (user: string) => void

type Param = Parameters<Func>

let p: Param = ['1'] // 正确
p = ['1', '2'] // 错误,只能包含一个字符串类型值

通过上面的示例可以看到通过 Parameters 获取到了 Func 的参数类型,并返回的是数组形式:[string]。因此,变量p的赋值就只能是包含一个字符串类型值的数组。

11. ConstructorParameters

ConstructorParameters 作用是用来获取一个类的构造函数参数类型,并以数组的形式返回。

在 TS 中的源码实现:

1
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

示例:

1
2
3
4
5
6
7
8
9
// 类 Foo 的构造函数有两个参数,第一个为 string 类型,第二个为 number 类型。
class Foo {
constructor(x: string, y: number){
console.log(x, y)
}
}

// 在使用 ConstructorParameters 处理之后,获取到的是一个类型数组。而且第一个值必须为 string 类型,第二个值必须为 number 类型。
const foo: ConstructorParameters<typeof Foo> = ['1', 2]

12. ReturnType

ReturnType 作用是用来得到一个函数的返回值类型。

在阅读源码之前我们需要了解一下 infer 这个关键字, 在条件类型语句中, 我们可以用 infer 声明一个类型变量并且对它进行使用, 我们可以用它获取函数的返回类型。

在 TS 中的源码实现:

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

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

示例:

1
2
3
4
// 用 ReturnType 获取到 Func 的返回值类型为 string,所以,foo 也就只能被赋值为字符串了
type Func = (value: number) => string

const foo: ReturnType<Func> = '1'

13. InstanceType

InstanceType 作用是获取一个类的实例类型,可以用获取到的实例类型来约束一个变量的赋值必须和类的成员类型完全一样才可以。

在 TS 中的源码实现:

1
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义的类 Foo 中有一个字符串类型的 x,一个数字类型的 y,一个参数为字符串类型的方法 say
class Foo {
public x = '1'
public y = 2

public say = (value: string) => {
console.log(value)
}
}

// 我们用 InstanceType 获取类 Foo 的实例类型,用来它约束变量 foo。那么,接下来给foo赋值时就必须完全符合 Foo 的成员类型才可以
const foo: InstanceType<typeof Foo> = {
x: '1',
y: 2,
say: (value: string) => {
console.log(value)
}
}

// 假设你将变量foo中的x值赋值为数字 1,就肯定会收到类型检查错误了:
// Type 'number' is not assignable to type 'string'.

14. 自定义类型别名(非内置类型定义)

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

  • DeepReadonly

    DeepReadonly 用来深度遍历 T,并将其所有属性变成只读类型。

    1
    type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> }
  • ConvertNumberToString

    ConvertNumberToString 用来将 number 转换为 string 类型。

    1
    2
    3
    type ConvertNumberToString<T> = {
    [K in keyof T]: T[K] extends string ? string : T[K]
    }
  • ValueOf

    ValueOf与keyof相对应。取出指定类型的所有 value。

    1
    type ValueOf<T> = T[keyof T]
  • Mutable

    Mutable 用来将 T 的所有属性的 readonly 移除。

    1
    2
    3
    type Mutable<T> = {
    -readonly [P in keyof T]: T[P]
    }

    其实还有对 + 和 -, 这里要说的不是变量的之间的进行加减而是对 readonly 进行加减。

  • PowerPartial

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

    1
    2
    3
    4
    5
    6
    type PowerPartial<T> = {
    // 如果是 object,则递归类型
    [U in keyof T]?: T[U] extends object
    ? PowerPartial<T[U]>
    : T[U]
    };

学习链接:

-------------本文结束感谢您的阅读-------------