chenxiaochun / blog

🖋️ChenXiaoChun's blog
181 stars 15 forks source link

TypeScript 内置工具泛型 #67

Open chenxiaochun opened 5 years ago

chenxiaochun commented 5 years ago

image

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

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

内置类型定义

Awaited

从名称来看,类似于async中的await或者是 Promise 上的then方法。它的作用就是可以递归的将 Promise 进行展开,以返回其最终类型:

type A = Awaited<Promise<string>>; // A 的类型为 string

type B = Awaited<Promise<Promise<number>>>; // B 的类型为 number

type C = Awaited<boolean | Promise<number>>; // C 的类型为 boolean | number

Record

Record在 TS 中的源码实现:

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

它用来生成一个属性为 K,类型为 T 的类型集合。如下所示,我用它生成了一个Foo类型,那么就表示所有指定为Foo类型的变量都必须包含一个 key 为a,value 为string类型的字段。否则,TS 类型检查器就会报错。

type Foo = Record<'a', string>

const foo: Foo = {a: '1'} // 正确
const foo: Foo = {b: '1'} // 错误,因为 key 不为 a
const foo: Foo = {a: 1} // 错误,因为 value 的值不是 string 类型

可以用Record来处理另外一种场景。假如我本来已经有了两个类型:

interface Foo {
  a: string
}
interface Bar {
  b: string
}

我想把FooBar两个类型的 key 合并到一起,并给它们重新指定成 number 类型,可以使用Record这样实现:

type Baz = Record<keyof Foo | keyof Bar, number>

此时,Baz的类型就相当于下面的类型定义了:

interface Baz {
  a: number
  b: number
}

Partial

Partial在 TS 中的源码实现:

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

它用来将 T 中的所有的属性都变成可选的。下面的示例中定义了一个类型IFoo,它拥有两个必选的属性 a 和 b。

interface IFoo {
  a: number
  b: number
}

const foo: IFoo = { a: 1 } // 错误,因为缺少 b 属性

在将变量foo指定为IFoo类型之后,它就必须同时包含 a 和 b 两个属性,否则就会报下面的类型检查错误:

Property 'b' is missing in type '{ a: number; }' but required in type 'IFoo'.

所以,如果我们能把IFoo的两个属性都变成可选的不就没问题了:

const foo: Partial<IFoo> = { a: 2 } // 正确,因为 IFoo 的属性都已经变成了可选状态

Required

Required在 TS 中的源码实现:

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

它的作用正好和上面的Partial相反,是将 T 中的所有属性都变成必选的状态。下面示例中定义的类型IFoo包含了两个可选的属性 a 和 b。所以,将变量foo指定为IFoo类型并且只包含一个 a 属性是不会有问题的。

interface IFoo {
  a?: number
  b?: number
}

// 正确,因为 IFoo 的两个属性本来就是可选的状态
const foo: IFoo = {
  a: 2
}

这时,我们可以用RequiredIFoo的所有属性都变成必选状态,否则,就报类型检查错误:

// 错误,因为 IFoo 的属性已经被变成了必选的状态
const foo: Required<IFoo> = {
  a: 2
}
Property 'b' is missing in type '{ a: number; }' but required in type 'Required<IFoo>'.

Readonly

Readonly在 TS 中的源码实现:

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

这个从字面意思就可以理解是将一个类型的所有成员变为只读的状态。看下面的示例,肯定可以随意给name赋值成别的字符串值。

interface IFoo {
  name: string
  age: number
}

const foo: IFoo = {
  name: 'cxc',
  age: 22,
}

foo.name = 'xiaoming'

Readonly转换一下:

const foo: Readonly<IFoo> = {
  name: 'cxc',
  age: 22,
}

foo.name = 'xiaoming' // 错误,因为 name 仅是只读的
foo.age = 20 // 错误,因为 age 也仅是只读的
Cannot assign to 'name' because it is a read-only property.

Pick

Pick在 TS 中的源码实现:

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

它的作用是从 T 中将所有的 K 取出来,并生成一个新的类型。下面示例中定义的IFoo类型包含了两个必选属性 a 和 b。所以,将foo指定为IFoo类型之后,就肯定必须包含这两个属性,否则就会报类型检查错误:

interface IFoo {
  a: number
  b: number
}

const foo: IFoo = {
  a: 1,
  b: 2,
}

但是,如果我想让foo只包含IFoo类型的 a 属性,就可以用Pick这样来实现。它就是告诉 TS 仅仅将 a 属性从IFoo中提取出来即可。

// 正确,使用 Pick 生成的新类型确实只包含 a 属性
const foo: Pick<IFoo, 'a'> = {
  a: 2,
}

// 错误,使用 Pick 生成的新类型中并不包含 b 属性
const foo: Pick<IFoo, 'a'> = {
  b: 2,
}

注意,它和上面的Partial不一样的地方在于,Partial是将类型中的所有的属性都变成了可选状态,而不能将某一个属性单独提取出来。

Exclude

Exclude在 TS 源码中的实现:

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

它的作用是从 T 中排除掉所有包含的 U 属性。如果不明白这句话,就看下面示例。

代码运行之后,TFoo只会包含一个 2。这是因为Exclude会从第一个类型参数中将其所有包含的第二个类型参数中的值给排除掉。我们可以看到在第一个类型参数中只包含第二个类型参数中的 1,因此,它就会被排除掉,只剩下 2 了。

type TFoo = Exclude<1 | 2, 1 | 3>

所以,如果一个变量被指定为了TFoo类型,它就只能被赋值为 2 了,否则就会报类型检查错误:

const foo: TFoo = 2 // 正确
const foo: TFoo = 3 // 错误,因为 TFoo 中不包含 3

Extract

Extract在 TS 中的源码实现:

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

它的作用正好和上面的Exclude相反。而是从 T 中提取出所有包含的 U 属性值。还是看下面的示例:

type TFoo = Extract<1 | 2, 1 | 3>

TFoo类型最终只会包含 1。这是因为 T 包含 U 中的属性值 1,Extract会将它提取出来生成一个类型,也就相当于:

type TFoo = 1

NonNullable

NonNullable在 TS 中的源码实现:

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

它的作用是去除 T 中包含的null或者undefined。如下示例所示,变量foo的类型为TFoo,因此下面的赋值都是没问题的。

type TFoo = 1 | null | undefined
let foo: TFoo = 1
foo = null
foo = undefined

如果我想把TFoo中的nullundefined去除掉,可以这样处理:

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

Parameters

Parameters的 TS 源码实现:

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

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

type Func = (user: string) => void

type Param = Parameters<Func>

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

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

ConstructorParameters

ConstructorParameters的 TS 源码实现:

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

它的作用是用来获取一个类的构造函数参数类型,并以数组的形式返回。如果不明白这句话的意思,看下面的示例。类Foo的构造函数有两个参数,第一个为 string 类型,第二个为 number 类型。

class Foo {
  constructor(x: string, y: number){
    console.log(x, y)
  }
}

在使用ConstructorParameters处理之后,获取到的是一个类型数组。而且第一个值必须为 string 类型,第二个值必须为 number 类型。

const foo: ConstructorParameters<typeof Foo> = ['1', 2]

ReturnType

ReturnType的 TS 源码实现:

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

它用来得到一个函数的返回值类型。看下面的示例用ReturnType获取到Func的返回值类型为string,所以,foo也就只能被赋值为字符串了。

type Func = (value: number) => string

const foo: ReturnType<Func> = '1'

InstanceType

InstanceType的 TS 源码实现:

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

它的作用是获取一个类的实例类型,可以用获取到的实例类型来约束一个变量的赋值必须和类的成员类型完全一样才可以。看下面示例定义的类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'.

Omit

Omit用来忽略 T 中的 K 属性

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

Uppercase<StringType>

Uppercase用来将每一个字符转换成大写形式

type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>

Lowercase<StringType>

Lowercase用来将每一个字符转换成小写形式

Capitalize<StringType>

Capitalize用来将第一个字符转换成大写形式

ShoutyGreeting的类型就变成了HELLO WORLD

非内置类型定义

下面这些并非是 TS 内置的类型定义,但是我觉得很实用的。所以,也想分享一下

DeepReadonly

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

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

ConvertNumberToString

ConvertNumberToString用来将number转换为string类型

type ConvertNumberToString<T> = {
  [K in keyof T]: T[K] extends string ? string : T[K]
}

ValueOf

ValueOfkeyof相对应。取出指定类型的所有 value

type ValueOf<T> = T[keyof T]

Mutable

用来将所有属性的readonly移除:

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

ThisParameterType

用来提取一个函数中的this参数的类型,如果没有this参数,则返回unknown

function toHex(this: Number) {
  return this.toString(16);
}

// 参加 n 为 number 类型
function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n);
}

OmitThisParameter

用来移除指定类型中的this参数

  1. 如果指定的类型中没有this,则直接返回该类型
  2. 如果有this,则会将this移除,再返回该类型
  3. 如果包含泛型,则会被忽略
  4. 如果有类型重载,则只会对最后一个进行操作
function toHex(this: Number) {
  return this.toString(16);
}

const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);

console.log(fiveToHex()); // fiveToHex 的类型为 const fiveToHex = () => string

ThisType

此类型不会返回任何转换之后的新类型。它只是用来对当前this上下文进行类型标注,在使用时必须开启[]()noImplicitThis

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return { ...data, ...methods } as D & M;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this
    },
  },
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

NestedKeyOf

此类型的作用是将如下类型:

type FormValues = {
  user: {
    name: string
    password: string
  }
}

转换为:

type Foo = ['user', 'name'] | ['user', 'password']
type NestedKeyOf<ObjectType extends object> = {
  [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
    ? [Key, ...NestedKeyOf<ObjectType[Key]>]
    : [Key]
}[keyof ObjectType & (string | number)]

相关文章

LesixCoder commented 3 years ago

type DeepReadonly = { readonly [P in keyof T]: P extends object ? DeepReadonly<T[P]> : never } 这样是不是比较好

chenxiaochun commented 3 years ago

@lsxlsxxslxsl ,P是属性名称,你这样写肯定返回都是never