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 behaviour; 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分配律:过滤联合Distribution: filtering a union示意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:把返回类型拆出来infer: pulling out the return type示意Illustrative
1// infer R:在匹配的模式里挖一个洞,编译器匹配成功时负责填上
2// (lib.es5.d.ts 里的 ReturnType 核心就是这一句;官方版还约束了 T extends (...args: any) => any,不匹配的分支写的是 any)
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// (the core of ReturnType in lib.es5.d.ts is this one line; the official one also constrains T extends (...args: any) => any, and its false branch is any)
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