TypeScript 类型体操之 Exclude

题目

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

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

例如:

ts 复制代码
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

实现

ts 复制代码
type MyExclude<T, U> =  T extends U ? never : T;

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

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

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

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

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

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

分布式条件类型

在这篇文章中对这个知识点解释得颇为详细,所以直接引用一下: juejin.cn/post/700036...

分布式条件类型(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

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

  • 裸类型参数(类型参数即泛型,见文章开头的泛型章节介绍)
  • 实例化
  • 分发到联合类型
typescript 复制代码
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 到了一点什么?上面的例子中泛型都是裸露着的,如果被包裹着,其条件类型判断结果会有什么变化吗?我们再看另一个例子:

typescript 复制代码
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"

    • 抽象下具体过程:

      typescript 复制代码
      ( 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 关键字的两侧都需要)。

相关推荐
用户214118326360215 分钟前
首发!即梦 4.0 接口开发全攻略:AI 辅助零代码实现,开源 + Docker 部署,小白也能上手
前端
gnip2 小时前
链式调用和延迟执行
前端·javascript
SoaringHeart2 小时前
Flutter组件封装:页面点击事件拦截
前端·flutter
杨天天.2 小时前
小程序原生实现音频播放器,下一首上一首切换,拖动进度条等功能
前端·javascript·小程序·音视频
Dragon Wu2 小时前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
Jinuss2 小时前
Vue3源码reactivity响应式篇之watch实现
前端·vue3
YU大宗师2 小时前
React面试题
前端·javascript·react.js
木兮xg2 小时前
react基础篇
前端·react.js·前端框架
ssshooter3 小时前
你知道怎么用 pnpm 临时给某个库打补丁吗?
前端·面试·npm
IT利刃出鞘4 小时前
HTML--最简的二级菜单页面
前端·html