Exclude<T, U> 从联合类型中移除成员,Extract<T, U> 只保留匹配的成员,NonNullable<T> 则移除 null 和 undefined。三者筛选的都是联合类型,不会动对象的属性。
和所有工具类型一样,它们只存在于编译期。最后一行会被编译器拒绝;实际运行的 JavaScript 根本不知道 Visible 是什么。
Exclude、Extract 与 NonNullable 一览
| 类型 | 保留 | 示例 | 结果 |
|---|---|---|---|
Exclude<T, U> | T 中不能赋值给 U 的成员 | Exclude<"a" | "b" | "c", "a"> | "b" | "c" |
Extract<T, U> | T 中能赋值给 U 的成员 | Extract<"a" | "b" | "c", "a" | "z"> | "a" |
NonNullable<T> | 除 null 和 undefined 外的一切 | NonNullable<string | null | undefined> | string |
关键在于“能赋值给 U”。U 不必是一份精确的成员列表;它可以是一个更宽的类型,能容纳好几个成员:
按结构挑选联合类型成员
在可辨识联合类型上,用 Extract 配合一个部分结构,就能取出具有该结构的变体。当联合类型不是你自己写的时候,这是给其中某个变体命名的常用方法:
结构 { type: "click" } 只是 click 变体的超类型,所以 Extract 保留的只有它。如果一个结构能匹配多个变体(比如有两个变体都有 { x: number }),它们都会被保留。
NonNullable
NonNullable<T> 是 !== null && !== undefined 检查在类型层面的版本。从某个可选的东西推导类型时,经常会用到它:
User["manager"] 是 { name: string } | null | undefined(? 加上了 undefined),而 NonNullable 把两者都去掉了。从 TypeScript 5.5 开始,编译器还能自己推断出这个谓词,所以 raw.filter((s) => s != null) 的类型已经是 string[];显式写出 s is NonNullable<typeof s> 能让意图一目了然,在旧版本中也能用。它的定义是 type NonNullable<T> = T & {}:空对象类型 {} 接受除 null 和 undefined 之外的所有值,所以和它取交叉,恰好去掉这两者。
它们是如何实现的
Exclude 和 Extract 在标准库中都是只有一行的条件类型:
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
当 T 是联合类型时,条件类型会分别作用于每一个成员(即分配),结果中的 never 成员会消失。所以 Exclude<"a" | "b", "a"> 变成 ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b"),也就是 never | "b",最终是 "b"。条件类型页面详细介绍了分配的机制。
Exclude 与 Omit,Extract 与 Pick
这几个名字很容易混淆,因为两组都是“移除”或“保留”某些东西。区别在于它们作用的对象:
| 作用于 | 移除或保留 | 示例 | |
|---|---|---|---|
Exclude<T, U> | 联合类型 | 联合类型成员 | Exclude<"a" | "b", "a"> → "b" |
Extract<T, U> | 联合类型 | 联合类型成员 | Extract<"a" | "b", "a"> → "a" |
Omit<T, K> | 对象类型 | 属性 | Omit<User, "password"> |
Pick<T, K> | 对象类型 | 属性 | Pick<User, "name"> |
对对象类型使用 Exclude 是一个常见的失误,而且它不会报错:
Account 不能赋值给 "password",所以 Exclude 把它原封不动地保留下来。Omit 本身就是把 Exclude 应用到键上构建的:Omit<T, K> 就是 Pick<T, Exclude<keyof T, K>>。更多内容见 Omit。
更严格的 Exclude
Exclude 的第二个参数接受任何类型,所以拼写错误时什么也不会移除,也不会报错。加上一个约束,就能让编译器检查你要排除的东西确实在联合类型里:
index.ts(7,37): error TS2344: Type '"drafft"' does not satisfy the constraint 'Status'.
改正拼写后就能通过编译,Strict 等于 "published" | "archived"。同样的技巧也适用于 Extract,以及严格版的 Omit(K extends keyof T)。
常见问题
TypeScript 中的 Exclude 有什么作用?
Exclude<T, U> 从联合类型 T 中移除所有可以赋值给 U 的成员。Exclude<"a" | "b" | "c", "a"> 的结果是 "b" | "c"。要移除多个成员,就给 U 传一个联合类型:Exclude<T, "a" | "b">。
Exclude 和 Extract 有什么区别?
它们正好相反。Exclude<T, U> 保留 T 中不能赋值给 U 的成员;Extract<T, U> 则只保留能赋值的那些成员。对于相同的 T 和 U,两个结果合起来就是 T。
Exclude 和 Omit 有什么区别?
Exclude 作用于联合类型,移除的是联合类型的成员。Omit 作用于对象类型,移除的是属性。Exclude<User, "password"> 起不到任何作用(它返回 User),而 Omit<User, "password"> 得到的是去掉该属性的对象类型。Omit 是基于 Exclude 实现的:它从 keyof T 中排除指定的键。
TypeScript 中的 NonNullable 有什么作用?
NonNullable<T> 从类型中移除 null 和 undefined:NonNullable<string | null | undefined> 的结果是 string。它的定义是 T & {},因为 {} 接受除 null 和 undefined 之外的所有值。
为什么 Exclude 一个不存在的成员时不会报错?
它的第二个参数没有约束,所以任何类型都能接受,像 Exclude<Status, "drafft"> 这样的拼写错误会悄无声息地什么也不移除。定义一个更严格的版本 type StrictExclude<T, U extends T> = Exclude<T, U>,让编译器检查 U 确实是 T 的一部分。