TypeScript 之 Distributive Conditional Types

简介

该特性直白来说:若在条件类型中给定的泛型为联合类型,则会将联合类型的每个成员分别执行条件运算。理解起来并不难,官方文档给了一个简单的示例:

type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;  // string[] | number[]
// 等价于执行了 (string extends any ? string[] : never) | (number extends any ? number[] : never)

另外,在文档的最后,还给出了如何禁用该特性的方法:

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

// 'StrArrOrNumArr' is no longer a union.
type StrArrOrNumArr = ToArrayNonDist<string | number>;

常见应用

  • IsNever
    实现一个类型工具 IsNever<T> 若 T 为 never 则返回 true 否则返回 false。若按普通思路这样写会发现达不到预期:
type isNever<T> = T extends never ? true : false;

type Test = isNever<never> // false

原因在于 never 会被当做联合类型进行分发,但是 never 会被当做一个没有任何成员的联合类型,也就没法分发了。按上面提到的方式禁用分发即可:

type isNever<T> = [T] extends [never] ? true : false;

type Test = isNever<never> // true

当然,除了官方提供的阻止分发的方式,还可以衍生出其它原理类似的方式,都是对原泛型进行 wrap:

type isNever1<T> = (() => T) extends (() => never) ? true : false;

// 注意: T 与 never 的位置与上面是不一样的,这涉及到函数参数的逆变
type IsNever2<T> = ((a: never) => void) extends ((a: T) => void) ? true : false;

type isNever3<T> = { a: T } extends { a: never } ? true : false;

type isNever4<T> = T[] extends never[] ? true : false;

// ...
  • OptionalKeys
    实现一个类型工具 OptionalKeys<T> 获取 Record T 中所有 optional key,例如:
type Test = OptionalKeys<{
    a?: 1,
    b: 2,
    c?: 3,
}>    //  'a' | 'c'

利用分发的特性,我们可以这么实现:

type OptionalKeys<T extends Record<string, any>,K = keyof T> = 
  // 分发所有的 key
  K extends keyof T ?
     // 逐一判断是否符合条件
      T extends Required<Pick<T,K>> ? 
        never
        : K
      : never;

借助同样的套路可以得到类似的其他几个工具函数:ReadonlyKeys<T>RequiredKeys<T>:

// Equal1<X, Y> from https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650
type Equal1<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<U>() => U extends Y ? 1 : 2) ? true : false;

type ReadOnlydKeys<T extends Record<string, any>,K = keyof T> = K extends keyof T ?
  Equal<Pick<T, K>, Readonly<Pick<T, K>>> extends true ? K : never
  :never;

type RequiredKeys<T extends Record<string, any>,K = keyof T> = K extends keyof T ?
  T extends Required<Pick<T,K>> ? K : never
  :never;

当然,对于本例,自行遍历是可行的:

type OptionalKeys<T extends Record<string, any>> = keyof {
    [key in keyof T as T extends Required<Pick<T, key>> ? never : key]:  1
}