DrillLab
第 102 / 105 道102 / 105 · DrillLab 自出By DrillLab

Exclude、Extract、ReturnType 是怎么实现的

How are Exclude, Extract and ReturnType implemented

先自己答,再往下看Answer it yourself first

一句话:三个都是条件类型(conditional type)—— 类型层面的三元表达式 T extends U ? X : Y。 Exclude 和 Extract 靠它的分配律(distributive)过滤联合; ReturnType 再加上 infer,从函数类型里拆出返回值。

分配律:T 是裸类型参数、 实参又是联合时,条件类型不把联合当整体判断, 而是逐个成员代入、再把结果并起来。Exclude<"a" | "b" | "c", "a"> 因此展开成三次判断: 命中的变成 never,而 never 是空联合, 并进结果就消失了。「过滤」的本质,是「把不要的变成 never」。

infer:extends 右边的模式里挖一个洞, 匹配成功时编译器负责把洞填上。T extends (...args: any[]) => infer R ? R : never里的 R 就是那个洞 —— T 匹配到函数类型时, R 被填成它的返回类型。同一招能拆数组元素、Promise 的值、 函数参数(Parameters 就是把洞挖在参数位置)。

会追问:「怎么关掉分配律?」—— 两边包一层元组:[T] extends [U],T 不再是裸的,联合就被当成整体。 「Exclude<T, T> 是什么?」——never,每个成员都被自己命中。 「never 传进去呢?」—— 还是 never:空联合循环零次。 三问全是分配律的推论,吃透一条规则就全能答。

In one line: all three are conditional types — a ternary at the type level, T extends U ? X : Y. Exclude and Extract filter unions through its distributive behavior; ReturnType adds infer to pull the return type out of a function type.

Distributivity: when T is a bare type parameter and the argument is a union, the conditional does not judge the union as a whole — it substitutes each member separately and unions the results. Exclude<"a" | "b" | "c", "a"> therefore expands into three checks: matching members become never, and never is the empty union, so it vanishes when merged back in. Filtering really means turning the unwanted members into never.

infer: it digs a hole in the pattern to the right of extends, and the compiler fills the hole when the match succeeds. In T extends (...args: any[]) => infer R ? R : never, R is that hole — when T matches a function type, R gets its return type. The same trick unpacks array elements, the value inside a Promise, or function parameters (Parameters is the same hole dug at the parameter position).

Follow-up: “How do you switch distributivity off?” — wrap both sides in a tuple: [T] extends [U], T is no longer bare, so the union is judged as one piece. “What is Exclude<T, T>?” — never: every member is matched by itself. “And feeding never in?” — still never: the empty union loops zero times. All three are corollaries of one rule — master distributivity and the whole set falls out.

TypeScript分配律:过滤联合示意Illustrative
1// 条件类型:类型层面的三元表达式
2type IsString<T> = T extends string ? true : false;
3type A = IsString<"hi">; // true
4type B = IsString<42>; // false
5
6// 分配律:裸类型参数遇到联合,逐个成员代入,再把结果并起来
7type NoA = Exclude<"a" | "b" | "c", "a">;
8// = ("a" extends "a" ? never : "a")
9// | ("b" extends "a" ? never : "b")
10// | ("c" extends "a" ? never : "c")
11// = never | "b" | "c"
12// = "b" | "c" ← never 是空联合,并进去就消失
13
14// 手写版各一行
15type MyExclude<T, U> = T extends U ? never : T;
16type MyExtract<T, U> = T extends U ? T : never;
1// A conditional type: the ternary expression, at the type level
2type IsString<T> = T extends string ? true : false;
3type A = IsString<"hi">; // true
4type B = IsString<42>; // false
5
6// Distribution: a bare type parameter over a union is applied member by member, then joined
7type NoA = Exclude<"a" | "b" | "c", "a">;
8// = ("a" extends "a" ? never : "a")
9// | ("b" extends "a" ? never : "b")
10// | ("c" extends "a" ? never : "c")
11// = never | "b" | "c"
12// = "b" | "c" ← never is the empty union, so it disappears in a join
13
14// One line each, written by hand
15type MyExclude<T, U> = T extends U ? never : T;
16type MyExtract<T, U> = T extends U ? T : never;
TypeScriptinfer:把返回类型拆出来示意Illustrative
1// infer R:在匹配的模式里挖一个洞,编译器匹配成功时负责填上
2// (lib.es5.d.ts 里的 ReturnType 就是这么写的)
3type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
4
5function getUser() {
6 return { id: 1, name: "Ada" };
7}
8
9type U = MyReturnType<typeof getUser>;
10// { id: number; name: string }
11
12// 同一招能拆任何复合类型
13type ElementOf<T> = T extends (infer E)[] ? E : never;
14type Unwrap<T> = T extends Promise<infer V> ? V : T;
15
16type N = ElementOf<number[]>; // number
17type S = Unwrap<Promise<string>>; // string
1// infer R: leave a hole in the pattern, and the compiler fills it in when the match succeeds
2// (this is exactly how ReturnType is written in lib.es5.d.ts)
3type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
4
5function getUser() {
6 return { id: 1, name: "Ada" };
7}
8
9type U = MyReturnType<typeof getUser>;
10// { id: number; name: string }
11
12// The same move takes apart any composite type
13type ElementOf<T> = T extends (infer E)[] ? E : never;
14type Unwrap<T> = T extends Promise<infer V> ? V : T;
15
16type N = ElementOf<number[]>; // number
17type S = Unwrap<Promise<string>>; // string