题目
题目很简单,实现内置的 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
关键字的两侧都需要)。