导读:TypeScript 不是给 JavaScript 加注解,是给 JavaScript 加类型系统
本文只抓一个问题:TypeScript 到底在 JavaScript 之上加了什么?答案不是"类型注解",而是一整套结构化类型系统。理解这个区别,是后续所有文章的前提。
一个反直觉的例子
1 | |
Cat 和 Dog 是两个完全不同的 interface,名字不同,语义不同,但 TypeScript 认为它们可以互相赋值。
把同样的事情放到 Java 里:
1 | |
Java 拒绝了这次赋值。即使方法签名完全一致,Cat 和 Dog 是两个独立声明的接口,它们之间没有继承关系,所以不兼容。
这就是 TypeScript 和 Java 类型系统的根本分歧。Java 用的是名义类型系统(nominal type system)——类型兼容性取决于类型的名字和声明关系。TypeScript 用的是结构化类型系统(structural type system)——类型兼容性取决于类型的结构,即它有哪些属性和方法。
为什么 TypeScript 选择了结构化类型
TypeScript 的设计目标不是发明一门新语言,而是给已有的 JavaScript 代码加上类型检查。JavaScript 本身就是按结构使用对象的:
1 | |
JavaScript 代码天然按"鸭子类型"运作——如果一个对象有 .json() 方法,就可以当作 Response 用,不需要它实现某个接口或继承某个基类。TypeScript 的结构化类型系统是对这种既有行为的形式化:把运行时的隐式结构检查提前到编译期做。
如果 TypeScript 选择了名义类型,现有的 JavaScript 代码几乎无法直接迁移——每个对象字面量都需要声明实现了哪个接口,每个函数参数都需要标注来自哪个类。这会从根本上改变 JavaScript 的编程方式,与"超集"定位矛盾。
类型系统的核心模型:类型是集合
理解 TypeScript 类型系统有一个第一性原理:类型是值的集合。
1 | |
在这个模型下,类型运算就是集合运算:
1 | |
赋值兼容性的判定规则也变得直观:如果 A 是 B 的子集,那么 A 类型的值可以赋给 B 类型的变量。
1 | |
这个集合模型是后续所有高级类型特性的基础——联合类型是并集,交叉类型是交集,条件类型是基于子集关系的分支,泛型约束是对集合范围的限制。
类型擦除:编译期存在,运行时消失
TypeScript 的类型信息只存在于编译期。编译器完成类型检查后,输出的 JavaScript 代码中不包含任何类型信息。这个过程叫类型擦除(type erasure)。
1 | |
1 | |
类型注解、interface、type alias、泛型参数——全部消失了。运行时的 JavaScript 引擎完全不知道 TypeScript 类型系统的存在。
类型擦除带来两个直接的工程影响:
第一,运行时不能用 TypeScript 类型做判断。typeof 在运行时只能区分 JavaScript 的 7 种原始类型和 object/function,无法区分 TypeScript 的 interface 或 type alias。如果需要在运行时验证数据结构(比如 API 返回值),必须借助运行时验证库(Zod、Valibot 等)或手写类型守卫。
1 | |
第二,TypeScript 的类型检查是零运行时开销的。不像 Python 的 typing 模块在运行时仍保留类型对象,TypeScript 的类型信息在编译后完全消失,不占用任何内存,不影响任何运行时性能。
实验:在 Playground 中观察结构化类型
打开 TypeScript Playground(https://www.typescriptlang.org/play),粘贴以下代码:
1 | |
观察几件事:
第一,Point3D 可以赋值给 Point2D 参数,因为 Point3D 拥有 Point2D 要求的全部属性(x 和 y)。按集合模型理解:所有 Point3D 值的集合是所有 Point2D 值的集合的子集——每个三维点都是一个有效的二维点(忽略 z),但反过来不成立。
第二,反向赋值报错。Point2D 缺少 z 属性,不满足 Point3D 的结构要求。
第三,切换到 Playground 右侧的 “.JS” 标签页,观察编译输出:所有 interface 声明消失了,函数参数的类型注解消失了,只剩下纯 JavaScript。
多余属性检查:结构化类型的例外
结构化类型的"子集可以赋给超集"规则有一个例外:对象字面量的多余属性检查(excess property checking)。
1 | |
同一个对象,直接用字面量赋值会报错,经过中间变量就不会。这不是 bug,而是 TypeScript 的有意设计。
对象字面量通常是"刚写出来的新对象",如果它包含了目标类型中不存在的属性,很可能是拼写错误(比如把 port 写成 prot)。TypeScript 对这种场景额外收紧检查,在编译期就捕获拼写错误。
而经过中间变量的对象已经有了自己的类型(由推断得到),此时 TypeScript 按标准的结构化兼容性判断——只要中间变量的类型是目标类型的超集,赋值就合法。
这个设计选择体现了 TypeScript 类型系统的实用主义倾向:结构化类型的灵活性和编译期错误检测之间的折中。
TypeScript 编译器做了什么
TypeScript 编译器(tsc)的工作流程可以压成四个阶段:
1 | |
解析阶段把 TypeScript 源码转换成抽象语法树(AST)。这一步只做语法分析,不涉及类型。
类型检查阶段是核心。编译器遍历 AST,为每个表达式推断类型,检查赋值兼容性、函数调用参数匹配、泛型实例化等。所有的类型错误都在这一步报告。
输出阶段做两件事:一是擦除类型生成 JavaScript 文件(.js),二是提取类型信息生成声明文件(.d.ts)。声明文件用于给其他 TypeScript 项目或 IDE 提供类型信息,自身不包含任何可执行代码。
截至 TypeScript 6.0(2026-03),编译器仍然用 JavaScript/TypeScript 实现。TypeScript 7.0(2026-07 GA)将编译器用 Go 重写,类型检查速度提升约 10 倍。但无论实现语言怎么变,上面四个阶段的逻辑结构不变。
TypeScript 的版本演进:从 5.x 到 7.0
截至 2026 年 7 月,TypeScript 的版本线经历了几个重要节点:
TypeScript 5.8/5.9 是日常开发中的主力版本。5.8 增加了 erasableSyntaxOnly 编译选项,禁止 enum、namespace 等 TypeScript 独有的运行时语法,推动项目向"纯类型注解"方向发展。5.9 稳定了 TC39 Decorator Metadata 提案,增加了 import defer 语法支持和 --module node20 模式。
TypeScript 6.0(2026-03 稳定)是 JavaScript 实现的最终版本。它的主要作用是为 7.0 迁移做准备:strict 默认开启、target 默认升到 es2025、moduleResolution node10 被标记废弃、target: es5 被标记废弃。6.0 还引入了 --stableTypeOrdering 标志,帮助开发者提前发现 6.0 和 7.0 之间类型排序差异导致的输出变化。
TypeScript 7.0(2026-07-08 GA)是 Go 原生编译器。核心变化是编译速度提升 8-12 倍(VS Code 代码库的类型检查从 125 秒降到 10 秒),内存占用降低 3 倍。Go 重写的架构决策是:不重新设计类型检查算法,而是逐文件将已有的 JavaScript 实现移植到 Go,保持相同的语义。性能提升来自两个叠加效应——原生机器码替代解释执行,加上 Go goroutine 实现的并行类型检查。
这个系列的代码示例默认基于 TypeScript 5.9/6.0 的行为。涉及 7.0 特有变化时会明确标注版本。
模式提炼
TypeScript 的结构化类型系统可以提炼为一个通用设计模式:契约由结构定义,而非由名字定义。
在名义类型系统中,两个类型的兼容性取决于它们是否显式声明了继承或实现关系。这像法律系统中的"身份认证"——不看你能做什么,只看你的证件。
在结构化类型系统中,两个类型的兼容性取决于它们是否具有相同的属性和方法签名。这像面试中的"能力评估"——不看你从哪毕业,只看你能不能完成任务。
这两种方式各有适用场景。名义类型在需要严格区分不同领域概念时更安全(比如"用户 ID"和"订单 ID"都是 string,但不应该互相赋值)。结构化类型在需要跨边界组合对象时更灵活(比如 API 返回的 JSON 对象不需要声明实现了哪个接口就能直接使用)。
TypeScript 选择结构化类型作为默认,同时通过品牌类型(branded types)等模式支持名义类型的使用场景,是一种"默认灵活、按需收紧"的策略。
工程迁移表
| TypeScript 概念 | Java 对应 | Rust 对应 | Haskell 对应 | 核心差异 |
|---|---|---|---|---|
| 结构化类型(structural typing) | 无直接对应;Java 是名义类型 | trait 实现(名义 + 结构混合) | type class(名义) | TS 按结构判断兼容性,其余按名字/声明 |
| interface | interface(名义) | trait(名义,需显式 impl) | type class(名义,需显式 instance) | TS 的 interface 不需要 implements 声明 |
| type alias | 无直接对应(Java 没有类型别名) | type alias(编译期展开) | type synonym(编译期展开) | TS 的 type alias 支持联合、交叉、条件等高级运算 |
| 类型擦除 | 泛型类型擦除(保留原始类型) | 无擦除(单态化) | 无擦除(字典传递) | TS 擦除所有类型信息,Java 只擦除泛型参数 |
unknown |
Object(运行时仍有类型) |
无直接对应 | 无直接对应 | TS 的 unknown 是编译期概念,运行时不存在 |
any |
无对应(Java 没有"关闭类型检查"的机制) | 无对应 | 无对应 | any 是 TypeScript 的逃生舱,关闭类型检查 |
never |
无对应(Java 用 void 或异常表达类似语义) | !(never type,nightly) |
Void(空类型) |
TS 的 never 是空集,用于穷尽检查 |
常见误解
误解一:TypeScript 是 JavaScript 的一种方言。
修正:TypeScript 是 JavaScript 的超集——每一段合法的 JavaScript 代码都是合法的 TypeScript 代码。TypeScript 没有改变 JavaScript 的运行时语义,只是在编译期增加了类型检查层。编译后输出的就是标准 JavaScript。
误解二:TypeScript 的类型检查会让代码变慢。
修正:类型擦除意味着运行时完全没有类型检查的开销。TypeScript 的类型系统是零运行时成本的。代价出现在开发期:编译器需要时间做类型检查。TypeScript 7.0 的 Go 原生编译器将这个开发期成本降低了约 10 倍。
误解三:TypeScript 的 interface 和 Java 的 interface 是一回事。
修正:Java 的 interface 是一种名义契约——类必须显式声明 implements SomeInterface 才能被视为该接口的实现。TypeScript 的 interface 是一种结构契约——任何拥有匹配结构的对象自动满足该 interface,无需任何声明。
误解四:用了 TypeScript 就不需要运行时数据验证了。
修正:TypeScript 的类型检查发生在编译期,只能约束编译期已知的代码。外部数据(API 响应、用户输入、数据库查询结果、JSON 解析结果)在编译期是未知的,TypeScript 无法保证这些数据符合预期类型。运行时验证(Zod、Valibot 等)仍然是必要的补充。
误解五:any 和 unknown 差不多,都是"任意类型"。
修正:any 关闭类型检查——对 any 类型的值可以做任何操作,编译器不会报错。unknown 保持类型检查——可以把任何值赋给 unknown 变量,但在使用之前必须通过类型收窄(typeof、instanceof 等)证明它的实际类型。unknown 是类型安全的,any 不是。
练习
练习 1:结构化类型兼容性判断
在 TypeScript Playground(https://www.typescriptlang.org/play)中预测以下代码哪些行会报编译错误,然后验证:
1 | |
练习 2:类型擦除观察
在 TypeScript Playground 中写一段使用泛型的代码,切换到 “.JS” 标签页观察编译输出。泛型参数在输出中还存在吗?类型约束呢?
1 | |
练习 3:多余属性检查边界
以下三种写法中,哪些会触发多余属性检查,哪些不会?为什么?
1 | |
系列导航
| 序号 | 主题 | 状态 |
|---|---|---|
| 00 | 导读:TypeScript 不是给 JavaScript 加注解,是给 JavaScript 加类型系统 | 本文 |
| 01 | 类型是集合:理解 TypeScript 类型系统的第一性原理 | 待写 |
| 02 | 类型收窄:从 unknown 到具体类型的检查路径 | 待写 |
| 03 | 类型推断:编译器怎样猜出你没写的类型 | 待写 |
| 04 | 结构化类型:鸭子类型的形式化 | 待写 |
参考资料
- TypeScript 官方手册——TypeScript for Java/C# Programmers:https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html
- TypeScript 官方手册——Type Compatibility:https://www.typescriptlang.org/docs/handbook/type-compatibility.html
- TypeScript 6.0 发布公告:https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/
- Dan Vanderkam. Effective TypeScript. O’Reilly, 2nd Edition 2024. Item 4: “Get Comfortable with Structural Typing”
- TypeScript Playground:https://www.typescriptlang.org/play
