TS中常用范型汇总

333 阅读9分钟

什么是范型

泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。

泛型通过一对尖括号来表示(<>),尖括号内的字符被称为类型变量,这个变量用来表示类型。 例如:

function copy<T>(arg: T): T {
  if (typeof arg === 'object') {
    return JSON.parse(
      JSON.stringify(arg)
    )
  } else {
    return arg
  }
}

交叉类型(&)

交叉类型说简单点就是将多个类型合并成一个类型。

联合类型(|)

联合类型的语法规则和逻辑 “或” 的符号一致,表示其类型为连接的多个类型中的任意一个。

类型别名(type)

前面提到的交叉类型与联合类型如果有多个地方需要使用,就需要通过类型别名的方式,给这两种类型声明一个别名。类型别名与声明变量的语法类似,只需要把 const、let 换成 type 关键字即可。

索引类型(Keyof)

keyof操作符可以用于获取某种类型的所有键,其返回类型是联合类型

对Object类型进行使用:

type Point = { x: number; y: number; 2: 2; '3': 3 }
type P = keyof Point  // "x" | "y" | 2 | "3"
const p: P = 3 // 是会有错误提示的。
 

对接口类型进行使用

interface IPoint {
  x: number
  y: number
  id: string
  isVisible: boolean
}
type P1 = keyof IPoint // "name" | "age" | "location"
type P2 = keyof IPoint[] // number | "length" | "push" | "concat" | ...
type P3 = keyof { [x: string]: IPoint } // string | number
type P4 = keyof any;    // string | number | Symbol 
// 注意P4 比 P3多了一种Symbol类型

除了接口,keyof操作符还可以用于操作类。

class CPoint {
  x: number = 0
  y: number = 0
  isVisible: boolean = false
  setVisible() {
    this.isVisible = true
  }
}

let some: keyof CPoint = "setVisible" // 用于表示 类定义时的字段

typeof

ts 中 typeof 后跟一个具体值,用于获取这个值的类型:

let s = 'hello'
let n: typeof s
// string
let f = (name: string) => {
  return 1
}
type TypeF = typeof f
// (name: string) => number
// 注意这里不是函数的返回类型

类型映射(in)

in 关键词的作用主要是做类型的映射,遍历已有接口的 key 或者是遍历联合类型。下面使用内置的泛型接口 Readonly 来举例。

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

interface Obj {
  a: string
  b: string
}

type ReadOnlyObj = Readonly<Obj>

类型约束(extends)

最早的泛型 extends 是在 ts1.8 中引入的。 最开始是用来限制泛型类型

function assign<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
    target[id] = source[id];
  }
  return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
assign(x, { b: 10, d: 20 });
assign(x, { e: 0 }); // Error

ts 2.8 中引入了 Conditional Types。 (下面是官方直译)

条件类型 T extends U ? X : Y 会被直接推导为 X 或者 Y,也可能由于条件依赖更多的类型而被推迟推导。是否直接推导或者延迟推导,取决于:

  1. 首先,给定类型 T' 和 U',他们分别是类型 T 和 U 的实例(如果 T、U 有类型参数,用 any 替换),如果 T' 不能被分配给 U',那么有条件类型最终被推导为 Y。直觉上,如果 T 的最大化实例都不能分配给 U 的最大化实例,那么我们会直接推导为 Y。

  2. 接下来,对于 U 中的推断(infer 关键字)声明引入的每个类型变量,通过从T推断到U(使用与泛型函数的类型推断相同的推断算法)来收集一组候选类型。对于给定的推断类型 V,如果有任意候选类型从协变位置推断出,那么推断类型 V 是这些候选类型的并集;不然,如果有任意候选从抗变位置推断出,推断类型 V 是这些候选类型的交集;否则,类型V 就是 never。

  3. 然后,给定一个类型 T 的实例 T'',其中所有推断类型变量都替换为上一步中推断的类型V,如果T''绝对能分配给U,那么推导为 X。除了没考虑类型变量以外,绝对分配关系与常规的分配关系一致。直觉上来说,当一个类型绝对能分配到另一个类型上时,我们说它能分配到那些类型的所有实例上。

  4. 最后,条件类型依赖更多的类型变量,那么类型推导被推迟。

先只看第一点,后面的2、3、4都是针对 infer

type TypeName<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : T extends undefined
  ? "undefined"
  : T extends Function
  ? "function"
  : "object";
  
//T0 string extends string 为 true,所以返回 'string';
type T0 = TypeName<string>; // "string"  

//T1 'a' 是 string 的一个实例化,就是第一点中说的T',此时U'是所有字符串的实例化代表,
//所以 'a' 肯定可以分配给 string,所以返回 'string';
type T1 = TypeName<"a">; // "string"

//T2 中, 同理,true 是 boolean 的实例化
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"

Distributive conditional types(条件分布类型)

我把这个分布条件类型也放入 extends 小节里。

被选中的类型为裸类型参数的条件类型称为分布式条件类型(即没有被诸如数组,元组或者函数包裹)。 实例化期间,分布条件类型自动分布在联合类型上。

例如,T extends U ? X : Y,类型参数为 A | B | C ,的 T 解析为(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;

type T20 = Boxed<string>; // BoxedValue<string>;
type T21 = Boxed<number[]>; // BoxedArray<number>;
type T22 = Boxed<string | number[]>; // BoxedValue<string> | BoxedArray<number>;
type T23 = Boxed<string> | Boxed<number[]>; // BoxedValue<string> | BoxedArray<number>;

T22 联合类型,就等于分开后再联合,就等于 T23。

接着来看下面例子:


type BoxedValue<T> = { value: T }
type BoxedArray<T> = { array: T[] }
type Boxed2<T> = T extends Record<string, number>
  ? BoxedArray<T[string]>
  : BoxedValue<T>
type T24 = Boxed2<{ [key: string]: number }> // BoxedArray<number>
type T25 = Boxed2<{ [key: string]: string }> // BoxedArray<{[key: string]: string}>

const t24: T24 = { array: [1, 2] }
const t25: T25 = { value: { key: 'value' } }

infer

ts2.8 提出,infer 关键字只能在 extends 语句中使用,表明一个需要推导的类型。对同一个类型可以进行多次 infer 推导。

比如,2.8中新增的 ReturnType:

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

ReturnType用于获取一个函数的返回类型,上面的意思是:如果 T 可以分配给 (...args: any[]) => infer R 这种类型的函数的话,那么返回类型 R,否则返回 any。在这里返回用了infer R 代表需要推断类型 R。

同样,也能“无限 extends”,比如:

type Unpacked<T> = T extends (infer U)[]
  ? U
  : T extends (...args: any[]) => infer U
  ? U
  : T extends Promise<infer U>
  ? U
  : T

协变位置推导

协变位置对于同一个类型变量多个候选类型如何推导出一个联合类型。例如:

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type T10 = Foo<{ a: string; b: string }>; // string
type T11 = Foo<{ a: string; b: number }>; // string | number

看这里,Foo 判断 T是否可分配配给 { a: infer U; b: infer U },这里 U 是 key a 和 b的推导类型。

T10 中的 a 和 b 都是 string。 结果是string 没有问题。

T11 中的 a 是string 和 b 是 number,Foo 就直接是 string 和 number 的联合类型(也就是string | number)!ts2.8中的 infer 就这么设计的!!!

这里需要先说明一下:对象型类型都可当做是协变类型。

抗变位置推导

抗变位置对于同一个类型变量多个候选类型如何推导出一个交集类型。(注意区别于协变位置的联合类型)

type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never
type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }> // string
type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }> // string & number

T20 中的 a、b的类型都是(x: string) => void ,所以 Bar 推导 x 的类型时(不是非要与原函数的参数名一致哈,不要误解),U 都是 string。

T21 中 a 中 x类型是 string,b 的是 number,但是 infer 的参数类型 U都是同一个,那么最终该是啥类型?哦不对,不该问,上面都写了。。。不过为啥是 string & number (其实就等于 never)呢?为啥?因为就是ts2.8中的 infer 就这么设计的!!!

这里需要先说明一下:函数型类型都可当做是抗变类型。

关于双向协变,也可以通过下面的参考来了解。

有了上面协变和抗变的基础理解,UnionToIntersection 介玩意儿就好理解了!

// 联合类型改为交叉类型
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never
// { a: string } | { b: number } => { a: string } & { b: number }
type Test = UnionToIntersection<{ a: string } | { b: number }>
  1. U extends any ? 那必须滴啊!TS 中啥都可以分配给 any!所以,现在变成了:(k: U) => void extends ((k: infer I) => void) ? I : never 返回类型 = I' => {a: string} | I'' => {b: number} (注意,这里不是分布式条件类型,分布式条件类型需要的是裸类型)

  2. 此时 I被推导出两个类型!怎么处理?第1步中,我们发现,它把本身是对象类型的类型{ a: string } | { b: number } 转为了一个函数的参数类型。

  3. 从一个函数推导类型,是满足抗变位置推导的,抗变位置推导,最终类型是:没错,你答对了,是交集!所以最终返回类型 = I' => {a: string} & I'' => {b: number} = {a: string} & {b: number} = {a: string; b: number}

工具泛型

Partial

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

Partial 用于将一个接口的所有属性设置为可选状态,首先通过 keyof T,取出类型变量 T 的所有属性,然后通过 in 进行遍历,最后在属性后加上一个 ?。

Required

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

Required 的作用刚好与 Partial 相反,就是将接口中所有可选的属性改为必须的,区别就是把 Partial 里面的 ? 替换成了 -?。

Record

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

Record 接受两个类型变量,Record 生成的类型具有类型 K 中存在的属性,值为类型 T。这里有一个比较疑惑的点就是给类型 K 加一个类型约束,extends keyof any,我们可以先看看 keyof any 是个什么东西。

大致一直就是类型 K 被约束在 string | number | symbol 中,刚好就是对象的索引的类型,也就是类型 K 只能指定为这几种类型。

Pick

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

Pick 主要用于提取接口的某几个属性。

Exclude

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

Exclude 的作用与之前介绍过的 Extract 刚好相反,如果 T 中的类型在 U 不存在,则返回,否则抛弃。

Omit

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

Omit 的作用刚好和 Pick 相反,先通过 Exclude<keyof T, K> 先取出类型 T 中存在,但是 K 不存在的属性,然后再由这些属性构造一个新的类型。

相关参考链接:

TS范型疑难杂症详解