关于 types-as-sets
这项 TypeScript 学习技能将类型视为值的集合、将可赋值性视为子集关系,用这一视角解释联合、交叉、`extends`、`never` 和 `unknown`。
主要功能
- 01将类型建模为值的集合
- 02解释联合类型与交叉类型
- 03将 `extends` 理解为子集关系
- 04阐释 `never` 与 `unknown`
- 05用集合思维分析可赋值性
适用场景
- 01排查可赋值性错误
- 02推理泛型约束
- 03理解条件类型
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill types-as-sets安装后在 Claude Code 中输入/types-as-sets确认已加载。
详细介绍
将类型视为值的集合
概述
类型是可能值的集合。可赋值性意味着子集关系。
将类型理解为集合,有助于你推理联合类型、交叉类型、extends 和 never。这种思维模型能让 TypeScript 的行为变得直观。
何时使用此技能
- 不理解为什么
A & B的属性比A或B多 - 不理解为什么
extends表示“子集” - 推理联合类型和交叉类型
- 使用
never或unknown类型 - 调试“不能赋值给”错误
铁律
ALWAYS think "subset" when you see "extends" or "assignable to".
请记住:
- 联合(
|)= 更大的集合(定义域的并集) - 交叉(
&)= 更小的集合(定义域的交集) extends=“是……的子集”never= 空集(没有值)unknown= 全集(所有值)
检测:“可赋值性”困惑
如果你对可赋值性错误感到困惑,请从集合的角度思考:
type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;
const ab: AB = 'A'; // OK: 'A' is in {'A', 'B'}
const ab12: AB12 = ab; // OK: {'A', 'B'} ⊆ {'A', 'B', 12}
declare let twelve: AB12;
const back: AB = twelve; // Error!
// {'A', 'B', 12} is NOT a subset of {'A', 'B'}
集合论思维模型
TypeScript 到集合论的转换
| TypeScript | 集合论 | 含义 |
|---|---|---|
never | ∅(空集) | 没有值 |
字面量类型 "A" | 单元素集合 {A} | 一个值 |
T1 | T2 | T1 ∪ T2(并集) | 属于任一集合的值 |
T1 & T2 | T1 ∩ T2(交集) | 同时属于两个集合的值 |
unknown | 全集 | 所有值 |
extends | ⊆(子集) | “包含于” |
| “可赋值给” | ⊆(子集) | “包含于” |
为什么交叉类型 & 会增加属性
乍看之下,这似乎有悖直觉:
interface Person { name: string; }
interface Lifespan { birth: Date; death?: Date; }
type PersonSpan = Person & Lifespan;
// PersonSpan has MORE properties, not fewer!
const ps: PersonSpan = {
name: 'Alan Turing',
birth: new Date('1912/06/23'),
death: new Date('1954/06/07'),
}; // OK
为什么? 因为我们求的是值的集合的交集,而不是属性的交集。
- Person = 所有带有
name属性的对象 - Lifespan = 所有带有
birth(以及可选的death)属性的对象 - Person & Lifespan = 同时具有这两组属性的对象
值的集合更小,但每个值的属性更多。
为什么联合类型 | 能保证的属性更少
type K = keyof (Person | Lifespan);
// ^? type K = never
// No keys are guaranteed on BOTH Person AND Lifespan
值的集合更大,但我们能依赖的属性更少。
extends 关键字
在 TypeScript 中,extends 表示“是……的子集”:
interface Vector1D { x: number; }
interface Vector2D extends Vector1D { y: number; }
interface Vector3D extends Vector2D { z: number; }
// Vector3D ⊆ Vector2D ⊆ Vector1D (as sets of values)
// A Vector3D IS-A Vector2D IS-A Vector1D
这也适用于泛型约束:
function getKey<K extends string>(val: any, key: K) { /* ... */ }
// K can be any subset of string:
getKey({}, 'x'); // OK: 'x' ⊆ string
getKey({}, Math.random() < 0.5 ? 'a' : 'b'); // OK: 'a'|'b' ⊆ string
getKey({}, 12); // Error: number ⊄ string
never 类型(空集)
never 是空集——它不包含任何值:
const x: never = 12;
// ~ Type 'number' is not assignable to type 'never'.
// never is useful for exhaustiveness checking
function assertNever(x: never): never {
throw new Error('Unexpected value: ' + x);
}
unknown 类型(全集)
unknown 是全集——所有值都可以赋值给它:
const x: unknown = 'hello'; // OK
const y: unknown = 42; // OK
const z: unknown = null; // OK
// But you can't use unknown without narrowing
const str: string = x; // Error: must narrow first
数组与元组
const list = [1, 2];
// ^? const list: number[]
const tuple: [number, number] = list;
// Error! number[] is not assignable to [number, number]
// Because: there exist number[] that aren't pairs ([], [1], [1,2,3])
集合 number[] 不是 [number, number] 的子集。
实际应用
理解泛型约束
// T must be a subset of objects with an 'id' property
function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
理解条件类型
// "If A is a subset of B, then X, else Y"
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true ('hello' ⊆ string)
type B = IsString<number>; // false (number ⊄ string)
抗压应对协议
1. “交叉类型应该具有更少的属性”
压力:“A & B 应该只有两者共有的属性”
**回应:**我们求的是值的集合的交集,而不是属性的交集。
**行动:**思考:“哪些对象同时满足两个接口?”
2. “extends 表示继承”
压力:“extends 讲的是类继承”
**回应:**在类型中,extends 表示“是……的子集”。
**行动:**阅读时,将“extends”替换为“是……的子集”。
危险信号——停下来重新考虑
- 认为
&会移除属性 - 认为
|会增加属性 - 将
extends与经典继承混淆 - 忘记对象类型是“开放的”(允许额外属性)
常见的自我辩解(都不成立)
| 借口 | 实情 |
|---|---|
| “交叉表示共有部分” | 它涉及的是值,而不是属性 |
| “extends 表示继承” | 在类型中,它表示“是……的子集” |
| “联合类型具有所有属性” | 联合类型只能保证共有属性 |
快速参考
| 类型表达式 | 集合解释 | 大小 |
|---|---|---|
never | 空集 | 0 个值 |
"A" | 单元素集合 | 1 个值 |
"A" | "B" | 并集 | 2 个值 |
string | 所有字符串 | ∞ 个值 |
unknown | 全集 | 所有值 |
A & B | 交集 | 通常更小 |
A | B | 并集 | 通常更大 |
核心要点
类型是值的集合。可赋值性就是检查子集。
理解这一点,TypeScript 的行为就会变得直观。extends 表示“是……的子集”,& 对值集合取交集(因此需要更多属性),| 对值集合取并集(因此能保证的属性更少)。
参考资料
内容基于 Dan Vanderkam 所著的《Effective TypeScript》第 7 条:将类型视为值的集合。