likes
comments
collection
share

TypeScript 类型体操之 Exclude

作者站长头像
站长
· 阅读数 14

题目

题目很简单,实现内置的 Exclude <T, U> 类型,但不能直接使用它本身。

从联合类型 T 中排除 U 的类型成员,来构造一个新的类型。

例如:

type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

实现

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

必须承认看到答案的那一刻确实很懵逼。。

经过前面几道题我已经学习到了 extends 在条件类型的用法:

T extends U ? never : T 表示如果 T 可以赋值给 U,那么结果类型就是 never,否则就是 T

但是很显然,在这里这个理解是错误的。

搜索一番后查到了一个解释:分布式条件类型

简单理解就是当 T extends U ? ... 中的 T 为联合类型时,会把联合类型中的每一个类型单独进行判断,然后再把结果组合成一个联合类型返回。

分布式条件类型

分布式条件类型(Distributive Conditional Types)实际上不是一种特殊的条件类型,而是其特性之一(所以说条件类型的分布式特性更为准确)。我们直接先上概念: 对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上

原文: Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation

先提取几个关键词,然后我们再通过例子理清这个概念:

  • 裸类型参数(类型参数即泛型,见文章开头的泛型章节介绍)
  • 实例化
  • 分发到联合类型
type TypeName<T> = T extends string
  ? 'string'
  : T extends number
  ? 'number'
  : T extends boolean
  ? 'boolean'
  : T extends undefined
  ? 'undefined'
  : T extends Function
  ? 'function'
  : 'object';

// "string" | "function"
type T1 = TypeName<string | (() => void)>;

// "string" | "object"
type T2 = TypeName<string | string[]>;

// "object"
type T3 = TypeName<string[] | number[]>;

我们发现在上面的例子里,条件类型的推导结果都是联合类型(T3 实际上也是,只不过因为结果相同所以被合并了),并且其实就是类型参数被依次进行条件判断后,再使用|组合得来的结果。

是不是 get 到了一点什么?上面的例子中泛型都是裸露着的,如果被包裹着,其条件类型判断结果会有什么变化吗?我们再看另一个例子:

type Naked<T> = T extends boolean ? "Y" : "N";
type Wrapped<T> = [T] extends [boolean] ? "Y" : "N";

// "N" | "Y"
type Distributed = Naked<number | boolean>;

// "N"
type NotDistributed = Wrapped<number | boolean>;
  • 其中,Distributed 类型别名,其类型参数(number | boolean)会正确的分发,即先分发到 Naked<number> | Naked<boolean>,再进行判断,所以结果是 "N" | "Y"

  • 而 NotDistributed 类型别名,第一眼看上去感觉TS应该会自动按数组进行分发,结果应该也是 "N" | "Y" ?但实际上,它的类型参数(number | boolean)不会有分发流程,直接进行 [number | boolean] extends [boolean] 的判断,所以结果是 "N"

现在我们可以来讲讲这几个概念了:

  • 裸类型参数,没有额外被[]包裹过的,就像被数组包裹后就不能再被称为裸类型参数。

  • 实例化,其实就是条件类型的判断过程,就像我们前面说的,条件类型需要在收集到足够的推断信息之后才能进行这个过程。在这里两个例子的实例化过程实际上是不同的,具体会在下一点中介绍。

  • 分发到联合类型:

    • 对于 TypeName,它内部的类型参数 T 是没有被包裹过的,所以 TypeName<string | (() => void)> 会被分发为 TypeName<string> | TypeName<(() => void)>,然后再次进行判断,最后分发为"string" | "function"

    • 抽象下具体过程:

      ( A | B | C ) extends T ? X : Y
      // 相当于
      (A extends T ? X : Y) | (B extends T ? X : Y) | (B extends T ? X : Y)
      
      // 使用[]包裹后,不会进行额外的分发逻辑。
      [A | B | C] extends [T] ? X : Y
      

      一句话概括:没有被 [] 额外包装的联合类型参数,在条件类型进行判定时会将联合类型分发,分别进行判断。

这两种行为没有好坏之分,区别只在于是否进行联合类型的分发,如果你需要走分布式条件类型,那么注意保持你的类型参数为裸类型参数。如果你想避免这种行为,那么使用 [] 包裹你的类型参数即可(注意在 extends 关键字的两侧都需要)。