【TypeScript 高级应用】ts中的关键字和操作符及常用类型推断方法

2023/03/08 09:30:55

keyof 索引类型查询与索引访问

索引类型查询操作符 keyof 可以用来取得一个对象接口的所有已知的公共属性名的联合:

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

type T = keyof Foo; // -> 'name' | 'age'

接口的某个属性的类型可以用 T[key] 的形式来访问,Foo["name" | "age"] 等价于 Foo["name"] | Foo["age"]

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

type A1 = Foo["name"]; // string
type A2 = Foo["age"]; // number
type A3 = Foo["name" | "age"]; // string | number
type A4 = Foo["name"] | Foo["age"]; // string | number

获取接口中不为 never 的类型:

type person = {
  ssr: never;
  name: string;
  age: number;
};

type ExcludeNever<T> = T[keyof T];
type a = ExcludeNever<person>; // string | number

in

in 可以遍历枚举类型:

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

配合 keyof 使用:

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

type Obj = {
  [p in keyof Foo]: any;
}; // -> {name: any, age: any}

配合索引访问可以访问到 T 上的所有属性和属性值类型:

interface Foo {
  name: string;
  age: number;
}
type test<T> = { [K in keyof T]: T[K] };
type a = test<Foo>;

这里 K 为 "name" | "age"T[K] 则为 T["name"]T["age"]

extends

extends 关键字在 ts 中有多重作用:

  • 继承
  • 泛型约束
  • 条件类型判断
    • 普通类型直接判断
    • 泛型+联合类型时使用分配律计算最终结果

继承

类的继承、接口的继承:

class person {}
class son extends person {}

泛型约束

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

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

type son = Pick<person, "name">;

Pick 接收一个类型 T 作为参数, P extends keyof T 表示 P 的取值范围为 keyof T 也就是 'name' | 'age'

条件类型判断(普通类型)

extends 左侧类型不为泛型类型或泛型类型不为联合类型时是普通条件类型判断。

type Human = {
  name: string;
};
type Duck = {
  name: string;
  age: number;
};
type Bool = Duck extends Human ? "yes" : "no"; // yes
type Bool2 = Human extends Duck ? "yes" : "no"; // no

需要注意的是这里的 A extends B 是指 A 可以分配给 B, 而不是说 AB 的子集。

Duck extends Human 为 true 是因为 Duck 包含了 Human 的所有属性,可以将 Duck 分配给 Human

条件类型判断(联合泛型)

extends 左侧类型由泛型参数传入且为联合类型时使用分配律计算最终结果。

type A1 = "x" extends "x" ? string : number; // string
type A2 = "x" | "y" extends "x" ? string : number; // number
type P<T> = T extends "x" ? string : number;
type A3 = P<"x" | "y">; // string | number

A1 和 A2 是 extends 条件判断的普通用法,和上面的判断方法一样。

A2 和 A3 的形式是一样的,区别在于 A3 的类型 T 以泛型参数形式传入,但是最终结果不同。

如果 extends 前面的参数是一个泛型类型,当传入的参数的是联合类型时,使用分配律计算最终的结果。

分配律是指,将联合类型的联合项拆成单项,分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果。

对于上面的例子 A3 来说,传入的 T"x" | "y",满足分配率,于是将 "x""y" 拆开,分别带入 P<T> 并将所有的结果再联合起来:

P<'x' | 'y'> => P<'x'> | P<'y'>
  • 阻止分配条件类型

    type Invoke<T> = T extends "x" ? string : number;
    type A = Invoke<"x" | "y">; // string | number;
    
    type Invoke2<T> = [T] extends "x" ? string : number;
    type A2 = Invoke2<"x" | "y">; // number
    

    在条件判断类型的定义中,将泛型参数使用 [ ] 括号起来,即可阻断条件判断类型的分配,此时的参数 T 被当做一个整体,不再分配。

  • 条件分配中的 never

    // never是所有类型的子类型
    type A1 = never extends "x" ? string : number; // string
    type P<T> = T extends "x" ? string : number;
    type A2 = P<never>; // never
    

    never 被认为是空的联合类型,也就是没有联合项的联合类型,然而因为没有联合项可以分配,所以 P<T> 的表达式其实根本没有执行,所以 A2 就的定义就类似于永远没有返回的函数一样,是 never 类型的。

通过例子理解类型分配

  • 获取 T 中有而 U 中没有的类型

    type Exclude<T, U> = T extends U ? never : T;
    type A1 = Exclude<"1" | "2" | "3", "2">; // "1" | "3"
    
    // 等价于 type A1 = Exclude<"1", "2"> | Exclude<"2", "2"> | Exclude<"3", "2">
    // Exclude<"1", "2"> // false -> "1"
    // Exclude<"2", "2"> // true -> never
    // Exclude<"3", "2"> // false -> "3"
    
  • 获取 T 和 U 中都有的类型

    type Extract<T, U> = T extends U ? T : never;
    type A1 = Exclude<"1" | "2" | "3", "2">; // "2"
    
    // 等价于 type A1 = Exclude<"1", "2"> | Exclude<"2", "2"> | Exclude<"3", "2">
    // Exclude<"1", "2"> // false -> never
    // Exclude<"2", "2"> // true -> "2"
    // Exclude<"3", "2"> // false -> never
    

infer

我们可以用 infer 声明一个类型变量来承载对应位置的类型并引用。

条件类型的基本语法是:

T extends U ? X : Y;

如果占位符类型 U 是一个可以被分解成几个部分的类型,譬如数组类型,元组类型,函数类型,字符串模板字面量类型等。这时候可以通过 infer 来获取 U 类型中某个部分的类型。

type InferArray<T> = T extends Array<infer U> ? U : never;

这段代码中用 infer U 来承载 T 的成员类型,如果有的话返回成员类型,否则返回 never

infer 的语法限制

infer 语法的限制为:

  1. infer 只能在条件类型的 extends 子句中使用。
  2. infer 的到的类型只能在 true 语句中使用,即 X 中使用。

+ 和 -

ts 中的 +- 用来操作类型而不是值。

去除类型中的 readonly

interface person {
  readonly name: string;
  age: number;
}

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

type son = Mutable<person>; // {name: string, age: number}

去除类型中的可选项:

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

应用场景

获取接口中所有函数类型的 key

interface Part {
  subparts: Part[];
  updatePart(newName: string): void;
  deletePart(newName: string): void;
}

type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

type a = FunctionPropertyNames<Part>; // "updatePart" | "deletePart"
  • [K in keyof T] 定义模板字面量类型 K,相当于 "subparts" | "updatePart" | "deletePart"

  • T[K] 索引访问,T["subparts"] 就是获取 T.subparts 的类型,其类型为 Part[]

  • T[K] extends Function ? K : never 如果 T[K] 的类型是 Function 则为 K 也就是模板字面量类型,否则为 never。

  • {[K in keyof T]: T[K] extends Function ? K : never;} 生成了一个新类型:

    {
      subparts: never;
      updatePart: "updatePart"; // 模板字面量类型
      deletePart: "deletePart"; // 模板字面量类型
    }
    
  • T[keyof T] 获取 T 中不为 never 的类型。

推断数组元素的类型

  • 推断所有元素的类型
type InferArray<T> = T extends Array<infer U> ? U : never;
type arr = Array<string | number>;
type item = InferArray<arr>; // string | number

type arr2 = [];
type item2 = InferArray<arr2>; // never

这段代码中用 infer U 来承载 T 的成员类型,如果有的话返回成员类型,否则返回 never

  • 推断数组第一个元素的类型
type InferFirst<T extends unknown[]> = T extends [infer P, ...infer _]
  ? P
  : never;
  • 推断数组最后一个元素的类型
type InferLast<T extends unknown[]> = T extends [...infer _, infer Last]
  ? Last
  : never;

推断函数的参数/返回值类型

  • 推断函数的参数类型
type InferParameters<T extends Function> = T extends (...args: infer R) => any
  ? R
  : never;

...args 代表的是函数参数组成的元组, infer R 表示用类型变量 R 来承接 ...args

  • 推断函数的返回值类型
type inferReturns<T extends Function> = T extends (...args: any) => infer R
  ? R
  : never;

将目标类型的所有属性变为必选项

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

interface person {
  name: string;
  age?: number;
}

type son = Required<person>; // {name: string, age: number}

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

-? 语法表示将代表可选项的 ? 去掉,从而让这个属性变为必选项,与之对应的还有个 +?,它用来将属性变为可选项,和 ? 效果一致。

从目标类型中提取一些 key 生成新类型

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

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

type son = Pick<person, "age">; // {age: number}

提取多个接口中的共有属性

  • 获取 T 中有而 U 中没有的类型

    type Exclude<T, U> = T extends U ? never : T;
    type A1 = Exclude<"1" | "2" | "3", "2">; // "1" | "3"
    
  • 获取 T 和 U 中都有的类型

    type Extract<T, U> = T extends U ? T : never;
    type A1 = Exclude<"1" | "2" | "3", "2">; // "2"
    

忽略目标类型中的一些 key

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

type Exclude<T, U> = T extends U ? never : T;
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type son = Pick<person, "age">; // {name: string}

参考

TS 一些工具泛型的使用及其实现open in new window

ts 中的 extendsopen in new window

TS 进阶之 inferopen in new window