mcpField
T
types-as-sets

types-as-sets

github.com/marius-townhouse开发644 次安装38 周前更新
代码仓库

关于 types-as-sets

这项 TypeScript 学习技能将类型视为值的集合、将可赋值性视为子集关系,用这一视角解释联合、交叉、`extends`、`never` 和 `unknown`。

主要功能

  1. 01将类型建模为值的集合
  2. 02解释联合类型与交叉类型
  3. 03将 `extends` 理解为子集关系
  4. 04阐释 `never` 与 `unknown`
  5. 05用集合思维分析可赋值性

适用场景

  1. 01排查可赋值性错误
  2. 02推理泛型约束
  3. 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 | T2T1 ∪ T2(并集)属于任一集合的值
T1 & T2T1 ∩ 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 条:将类型视为值的集合。

评论

支持 Markdown