用集合论真正理解 TypeScript 类型

TypeScript1周前发布 admin
117 0

TypeScript 的时候,大部分人是这么学联合类型和交叉类型的:

| 表示”或”,& 表示”并且把两边的属性合并在一起”。

这套说法背下来能应付日常写代码,但一旦碰到稍微复杂点的场景——比如为什么两个字符串字面量类型相交会变成 never,为什么条件类型作用在联合类型上会自动”分裂”——就开始懵了。

其实 TypeScript 的类型系统背后,有一套非常朴素的数学模型:集合论。一旦你把”类型”理解成”一堆值的集合”,前面那些看起来反直觉的规则,全都会变得理所当然。这篇文章就带你换个视角,重新认识一遍 TypeScript 的类型系统。

一、类型的本质:一个装着”合法值”的筐

先抛开语法,问一个最朴素的问题:一个类型到底是什么?

答案是:一个类型,就是所有满足这个类型的值组成的集合。

举几个例子感受一下:

  • boolean 类型,对应的集合是 {true, false}——一个只有两个元素的有限集合。
  • number 类型,对应的集合是所有能表示的数字组成的集合——非常大,但依然是个集合。
  • 字面量类型 "success",对应的集合是 {"success"}——只有一个元素的集合,专业说法叫”单例集合”。

再看两个特殊类型:

  • never:对应空集 。没有任何值属于 never,所以任何用到 never 的地方都进不去(比如一个永远不会执行到的 return)。
  • unknown:对应全集。它包含了这个世界上所有可能的值,任何东西都可以赋值给 unknown

把这几个对应关系记在脑子里,后面的规则会自动”推导”出来,不需要死记硬背。

二、联合类型 |:集合的并集

typescript:
type Status = "success" | "error" | "loading";

这行代码在做的事情,就是把三个单元素集合 {"success"}{"error"}{"loading"}并集,拼成一个含三个元素的大集合 {"success", "error", "loading"}

再看一个更”大”的例子:

typescript:
type ID = string | number;

这里是把 string 这个巨大的集合和 number 这个巨大的集合并在一起,得到一个更大的集合——覆盖了所有字符串和所有数字。

说白了,| 就是日常说的”或者”:”要么是这个,要么是那个”,谁都行,全都算数。

三、交叉类型 &:集合的交集(这里最容易搞错)

大部分人对交叉类型的直觉理解是:A & B 就是”把 A 和 B 的字段拼在一起”。

typescript:
interface Name { name: string }
interface Age { age: number }

type Person = Name & Age; // { name: string; age: number }

看起来确实像是”合并字段”,但这只是表面现象。真正发生的事情是求交集:一个值要同时”属于” Name 的集合,又”属于” Age 的集合,就必须同时满足两边的条件——既要有 name 字段,又要有 age 字段。所以”字段合并”其实是”同时满足两个集合条件”的结果,而不是原因。

这个视角的好处是,它能立刻解释一个新手常见的困惑:

typescript:
type Impossible = string & number; // never

为什么两个基础类型相交会得到 never?因为不存在任何一个值,既是字符串又是数字。string 集合和 number 集合根本没有交集,交集是空集,对应到类型系统里,就是 never

一旦用集合论理解,这个结果就不再是”奇怪的边界情况”,而是必然的数学结论。

四、子类型:其实就是子集

TypeScript 判断”这个值能不能赋值给那个类型”,本质上是在问一句话:

这个值所在的集合,是不是目标类型集合的子集?

来看一个例子:

typescript:
interface Animal { name: string }
interface Dog { name: string; breed: string }

let d: Dog = { name: "旺财", breed: "柴犬" };
let a: Animal = d; // 合法

Dog 要求的条件比 Animal 更多(多了 breed),所以满足 Dog 条件的值,数量上一定比满足 Animal 条件的值要少——Dog 集合是 Animal 集合的子集。反过来,一只 Animal 却不一定是 Dog(可能没有品种信息)。

这也是为什么”条件更多、约束更严”的类型反而能赋值给”条件更少、约束更宽松”的类型——听起来有点反直觉,但从集合的角度看非常自然:子集里的每个元素,天然也是父集合的元素。这正是 TypeScript “结构化子类型(Structural Typing)”规则背后的数学解释。

五、extends、泛型约束、条件类型:问的都是同一个问题

理解了子集关系,你会发现 TypeScript 里好几个看起来不相关的语法,其实都在问同一句话——”A 是不是 B 的子集”:

typescript:
// 泛型约束:T 必须是 string 集合的子集
function double<T extends string>(x: T) { ... }

// 条件类型:判断 T 是不是 string 的子集
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true,因为 {"hello"} 是 string 的子集
type B = IsString<number>;  // false,number 集合不是 string 集合的子集

顺带一提一个容易踩坑的细节:如果 T 是一个联合类型,条件类型会把联合类型拆开,对每个成员分别做子集判断,再把结果重新并起来,这叫”分布式条件类型”:

typescript:
type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// 结果是 string[] | number[],而不是 (string | number)[]

原因也很直白:对于并集来说,”整体是不是子集”和”逐个元素是不是子集,再把结果拼起来”,在数学上是等价的。TypeScript 干脆就用后一种方式实现(如果不想要这个行为,把 T 用元组包一层,比如 [T] extends [any] ? ... : ...,就能阻止这种拆分)。

六、类型收窄:往集合里做过滤

写代码时经常见到这种模式:

typescript:
function f(x: string | number) {
  if (typeof x === "string") {
    // 这里 x 被收窄成了 string
  }
}

用集合论看,这就是一次过滤x 原本属于 string | number 这个较大的集合,typeof x === "string" 这个判断把它筛到了一个更小的子集 string 里。类型收窄,做的事情跟你写 filter 函数没有本质区别,只不过过滤的是”类型”而不是”数组”。

七、可辨识联合:把一个大集合切成互不重叠的几块

typescript:
interface Circle { kind: "circle"; radius: number }
interface Square { kind: "square"; side: number }
type Shape = Circle | Square;

这里的 Shape 是一个大集合,被 kind 字段切成了两个互不相交的子集:kind"circle" 的一块,和 kind"square" 的一块。TypeScript 常说的”穷尽性检查(exhaustiveness check)”,本质上就是在验证:这几块子集的并集,是不是刚好把 Shape 这个大集合完整覆盖了,不多也不少。

八、any:一个不讲道理的例外

前面所有规则都很和谐,直到 any 出现。

按理说,any 应该表现得像”全集”,可以装下所有值。但它的实际行为是:任何类型都能赋值给 anyany 也能反过来赋值给任何类型。 这在集合论里其实是矛盾的——一个真正的全集,只能”包含”别人,不可能反过来被随便一个子集”倒灌”回去。

所以准确地说,any 并不是一个真正遵守集合规则的类型,它更像是 TypeScript 主动开的一个”逃生舱口”:一旦用上它,编译器就直接放弃对这块代码做集合运算和类型检查。这也是为什么大家都建议少用 any——它不是”集合论世界”里的正常公民,而是绕开整个类型系统的后门。

小结

把整篇文章的对应关系放在一起看一遍:

TypeScript 概念 集合论概念
类型 一堆值组成的集合
A | B 并集
A & B 交集
extends / 泛型约束 / 条件类型 子集判断
类型收窄 对集合做过滤
never 空集
unknown 全集
any 不讲规则的例外

下次再写类型定义、看条件类型报错、或者纠结”这两个类型为什么能互相赋值”的时候,不妨在脑子里画一个维恩图。你会发现,很多以前靠死记硬背的规则,其实一直都摆在那里,等着被看懂而已。

© 版权声明

相关文章

暂无评论

none
暂无评论...