TypeScript 类型系统 — 从结构类型到 any、unknown 与组合类型¶
这一章讲什么: 上一章说了「静态类型好」,这一章把 TypeScript 自带的类型工具箱 整个摊开:类型怎么工作、每个内置类型解决什么问题、什么时候必须绕开类型系统以及怎么绕才安全。 读完你能看懂大多数 TypeScript 代码里的类型标注,也能判断一段代码里的 any 是不得已还是偷懒。
1. 顶层全景:一个只认形状、不认名字的检查员¶
先建立本章最重要的一个心智模型。原书在开头就点破:JavaScript 其实有类型 (数字、字符串、布尔、数组……),只是不在声明时写明,只在运行时根据值临时推断—— 而且随时可以改写:今天装 5,明天装 "five",没人拦1。
TypeScript 的做法是声明时定死。而它的检查方式在静态类型语言里少见: 编译器只看类型的形状(shape),完全不看名字2。
原书的主线例子(下面整章反复用它):
class Person { name: string = ""; }
const jill: { name: string } = { name: "jill" };
const person: Person = jill; // ← 一个类,一个匿名形状,居然兼容
Person 和 { name: string } 是两个不同的名字,但编译器不抱怨——
因为两者的形状一模一样:都有一个 name 字段,都是字符串3。
这种只比形状的做法叫结构类型(structural typing),
俗语叫 duck typing:走路像鸭子、叫起来像鸭子,那它就是鸭子2。
检查员的判据(本章全景)
别的静态类型语言: 你叫 Person 吗?不叫 → 不兼容
TypeScript: 你有 name: string 吗?有 → 兼容,你叫什么都行
图说:名字只是给人看的标签,形状才是给编译器看的合同。
这一条贯穿后面所有内容:交集为什么能「合并」、接口为什么能互相替代, 根子都在「形状即类型」。
2. any:退出类型系统的逃生门,而且会传染¶
工具箱里有两个「兜底」类型,先讲危险的那个。
any 的意思是退出静态类型检查:声明成 any 的变量,可以装任何东西、
可以改任何东西,编译器一概不管——等于没有类型4。它存在的理由是现实的:
有时数据来自别人的 API,类型由别的团队、甚至别的公司控制,还经常变;
这种时候你需要一条逃生通道5。
但原书反复强调一个关键事实:any 会传染。 编译器不只不检查它,
还允许它的值无声无息地流进别的变量——它流到哪里,哪里的类型安全就被关掉6。
主线例子接着走。同一段代码,只看 any 的下场:
let val: any = 22;
val = "string value"; ← any 允许:改装字符串
val = new Array(); ← any 允许:再改装数组
val.doesnotexist(33); ← 数组上根本没有这个方法!
编译器(tsc):静默通过,零报错
VSCode:悬停上去只显示 any,连提示都没有
运行时:val.doesnotexist is not a function —— 到这一步才炸
打错一个方法名(书里举的就是 doesnotexist 这种手滑),编译器是你的最后一道防线;
而 any 把这道防线拆了——错误推迟到用户那里才出现,这正是第 01 章说的大忌7。
所以多数项目会开 noImplicitAny 编译选项:凡是忘了标注、被隐式当成 any 的地方,
一律报错8。判据很简单:any 只在「实在没有别的办法建类型」时才能用9。
3. unknown:同一条逃生门,但门口站了保安¶
any 的替代者是 unknown(TypeScript 3 引入):同样什么值都能装,
但在证明它的真实类型之前,几乎什么操作都不许做——不能读属性、不能调用、
不能赋给别的变量10。
把主线例子的主角换成 unknown:
let val: unknown = 22;
val = "string value";
val = new Array();
val.push(33); ← 编译器当场报错:unknown 上不能乱动
加一道检查(这叫 type guard,类型守卫):
if (val instanceof Array) { val.push(33) } ← 编译器放行
图说:unknown 像一个贴了封条的包裹——里面什么都能装,
但开封之前,连「能不能 push」都要先问一句。
instanceof 就是「问一句」的语法:运行时确认 val 确实是数组,编译器才在
这个分支里把它当数组对待。原书明说:这机制有点繁琐,但比 any 安全得多,
因为是编译器在把关,而且这是 TypeScript 团队官方推荐的做法11。
any 和 unknown 的对照,是全书最值钱的一张小表:
| 装什么 | 不检查就操作 | 值流入其他变量 | 安全性 | |
|---|---|---|---|---|
any |
任意 | 允许 | 悄无声息地传染 | 无 |
unknown |
任意 | 禁止,要先证明类型 | 禁止(除赋给 any/unknown) | 有 |
4. 交集、并集、字面量:把旧类型拼成新类型¶
结构类型「只看形状」的直接推论:类型可以像积木一样拼。
交集类型用 & 拼接:{ name: string } & { age: number } 表示
「同时满足两者」。名字容易误读——交集包含两个类型的全部字段,不只是公共字段;
「交」指的是「同时满足两个类型的那些值」12:
并集类型用 |,意思相反——「或者」:null | { name: string } 表示
这个变量要么是 null,要么是那个对象,二选一,但不能同时13。
用并集之前必须先缩窄(narrow)类型:想读 .name,先写
if (unionObj !== null) 排除掉 null 那一支,编译器才放行13。
字面量类型把「值本身」变成类型:"tom" | "linda" | "jeff" | "sue"
表示这个变量只能取这四个字符串之一,赋个 john 就编译报错14。
它几乎总是和并集搭配——用类型表达「这个字段只允许这几个值」(枚举)的轻量写法。
类型别名(type)不给类型起新名字,只是给已有的类型一个更好用的名字:
别名可用于几乎任何类型:并集、交集、元组、函数、泛型15。
它和 interface 有分工(后者专管对象形状,区别见下一章),但「给复杂形状一个名字」
这件事两者都常干。
5. 函数也是类型:void 与 never 的分界线¶
函数在 JavaScript 里是一等公民(可以像值一样传来传去),所以它们也需要类型16。
函数类型的写法长这样:(miles: number) => boolean——括号里是参数,
=> 表示「这是一个函数」,后面是返回类型。可以起成别名复用:
type Run = (miles: number) => boolean;
let runner: Run = function (miles: number): boolean { return miles > 10; };
runner(9); // false
一个小细节:类型里的参数名(miles)只是文档——编译器按位置和类型配对,
实现里换个参数名完全没问题17。
返回类型标在参数括号后:function runMore(distance: number): number。
它的作用不只是可读:函数体里万一返回了错误类型,编译器当场抓住18。
接下来是两个容易混的类型,分界线在「函数有没有跑完」:
function oldEnough(age: number): never | boolean {
if (age > 59) { throw Error("Too old!"); } // 抛错:这条路径不返回
if (age <= 18) { return false; }
return true;
}
never | boolean 实际就是 boolean——never 在并集里会被吸收。
它的用处是表达意图:读到 (): never 就知道这个函数不会正常返回,
而且编译器会把调用点之后的代码当成「永远走不到」21。
6. 作者的判断与证据¶
any应尽量避免、优先用unknown——这是作者立场,也是 TypeScript 官方团队的 推荐11。证据是演示性的:any 版代码编译器零报错、运行才炸; unknown 版代码编译器在写代码时就拦下7。- 结构类型的重要性是作者反复提醒的判断:「后文会看到这行为为什么必须时刻记着」2。
- 显式标注优于推断:原书说「一般你会显式设置类型」1,但没展开辩论—— 行业里对「什么时候该让编译器推断」有争议(见下节)。
7. 边界与局限¶
unknown的繁琐没给出路:每次使用都要写instanceof之类的守卫, 原书承认繁琐,只给了「更安全」作为回报;对大型 API 响应,更顺手的做法是 用类型自动推导库(如 zod)——书里没提,属于本书出版后的常见实践 (补充(不在书里):zod 等 schema 校验库在 2026 年已是校验外部数据的通行做法之一)。- 「显式优于推断」写得轻:业界通行口径其实是「让编译器推断,复杂公共接口才显式标注」; 原书的口径对初学者更安全,但不是唯一答案。
- 本章只覆盖到
never;Classes、Interfaces等 OOP 深水区在下一组原书节,见本拆解 03 章。
判断(我们的,不是书里的): 把
any与unknown的对照放在一章里讲, 并用同一个变量val演示两遍,是本章最好的教法——它让读者亲手摸到 「类型安全的开关在编译器手里,而 any 把这个开关交给了运行时」。 如果错,会错在: 在类型体操(复杂条件类型、模板字面量类型)盛行的今天, 「any = 偷懒、unknown = 谨慎」的黑白划分对高级用法过于简单——有些场景 any 是合理的过渡状态。但教学顺序上,先立规矩再谈例外,仍然是对的。
8. 可带走的¶
- TypeScript 检查类型只看形状不看名字——结构类型 / duck typing。
any退出类型检查且会传染;项目里应开noImplicitAny把漏网的 any 变成错误。unknown同样什么都能装,但用之前必须用类型守卫(如instanceof)证明类型。&交集 = 字段合并(取并集);|并集 = 二选一;用并集前先缩窄。- 字面量类型
"a" | "b"+ 别名,就是「枚举合法值」的轻量写法。 - 函数类型写法
(参数) => 返回类型;类型里的参数名只是文档。 void= 跑完但没值;never= 没跑完(抛错或死循环),编译器会标出其后代码不可达。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 结构类型与 duck typing | How do types work? | text/06-fm-how-do-types-work.txt:181(搜「structural typing」) |
| any 的定义与传染性 | The any type | text/07-fm-the-any-type.txt:5(搜「opts out」) · text/07-fm-the-any-type.txt:19(搜「silently disabling」) |
| noImplicitAny | The any type | text/07-fm-the-any-type.txt:29(搜「noImplicitAny」) |
| unknown 的语义 | The unknown type | text/08-fm-the-unknown-type.txt:23(搜「cannot perform almost any operation」) |
| any 版编译通过、运行才炸 | The unknown type | text/08-fm-the-unknown-type.txt:195(搜「no errors」) · text/08-fm-the-unknown-type.txt:213(搜「never what we want」) |
| 类型守卫与官方推荐 | The unknown type | text/08-fm-the-unknown-type.txt:289(搜「type guards」) · text/08-fm-the-unknown-type.txt:59(搜「recommended by the TypeScript team」) |
| 交集类型(含字段合并说明) | Intersection and union types | text/09-fm-intersection-and-union-types.txt:11(搜「combining」) · text/09-fm-intersection-and-union-types.txt:15(搜「all the fields」) |
| 并集与缩窄 | Intersection and union types | text/09-fm-intersection-and-union-types.txt:112(搜「let unionObj」) · text/09-fm-intersection-and-union-types.txt:138(搜「narrow」) |
| 字面量类型 | Literal types | text/10-fm-literal-types.txt:5(搜「constrain」) |
| 类型别名 | Type aliases | text/11-fm-type-aliases.txt:9(搜「another name」) |
| 返回类型声明与 void | Function return types | text/12-fm-function-return-types.txt:30(搜「readability」) · text/12-fm-function-return-types.txt:60(搜「should not use」) |
| 函数类型签名 | Functions as types | text/13-fm-functions-as-types.txt:19(搜「type Run」) · text/13-fm-functions-as-types.txt:60(搜「by position and type」) |
| never 与 void 的分界 | The never type | text/14-fm-the-never-type.txt:15(搜「for a function that never returns in the」) · text/14-fm-the-never-type.txt:75(搜「unreachable」) |
-
出处:「How do types work?」第 3–17 段(
text/06-fm-how-do-types-work.txt:3,搜「JavaScript does have types」;:17,搜「count = \"five\"」)。 ↩↩ -
出处:「How do types work?」第 177–181 段(
text/06-fm-how-do-types-work.txt:179,搜「not concerned with its name」)。 ↩↩↩ -
出处:「How do types work?」第 45–52 段与第 48 段(
text/06-fm-how-do-types-work.txt:48,搜「name: string」;:96,搜「does not complain」)。 ↩ -
出处:「The any type」第 5–17 段(
text/07-fm-the-any-type.txt:5,搜「opts out」;:17,搜「no type」)。 ↩ -
出处:「The any type」第 37–49 段(
text/07-fm-the-any-type.txt:39,搜「controlled by some other team」;:43,搜「escape hatch」)。 ↩ -
出处:「The any type」第 19 段(
text/07-fm-the-any-type.txt:19,搜「silently disabling」)。 ↩ -
出处:「The unknown type」第 195–213 段(
text/08-fm-the-unknown-type.txt:195,搜「succeeds with no errors」;:213,搜「never what we want」)。 ↩↩ -
出处:「The any type」第 27–35 段(
text/07-fm-the-any-type.txt:29,搜「noImplicitAny」)。 ↩ -
出处:「The any type」第 51–57 段(
text/07-fm-the-any-type.txt:57,搜「no other way」)。 ↩ -
出处:「The unknown type」第 19–27 段(
text/08-fm-the-unknown-type.txt:23,搜「cannot perform almost any operation」)。 ↩ -
出处:「The unknown type」第 59 段与第 289–293 段(
text/08-fm-the-unknown-type.txt:59,搜「recommended by the TypeScript team」;:289,搜「type guards」)。 ↩↩ -
出处:「Intersection and union types」第 11–17 段(
text/09-fm-intersection-and-union-types.txt:11,搜「combining」;:15,搜「all the fields」)。 ↩ -
出处:「Intersection and union types」第 112–140 段(
text/09-fm-intersection-and-union-types.txt:112,搜「let unionObj」;:138,搜「narrow」)。 ↩↩ -
出处:「Literal types」第 5–11 段与第 5 段(
text/10-fm-literal-types.txt:5,搜「constrain」;:19,搜「tom\" | \"linda」)。 ↩ -
出处:「Type aliases」第 9–11 段与第 63–67 段(
text/11-fm-type-aliases.txt:9,搜「another name」;:65,搜「unions, intersections」)。 ↩ -
出处:「Functions as types」第 3–7 段(
text/13-fm-functions-as-types.txt:3,搜「entire function signature」;:5,搜「first-class」)。 ↩ -
出处:「Functions as types」第 56–60 段(
text/13-fm-functions-as-types.txt:60,搜「by position and type」)。 ↩ -
出处:「Function return types」第 28–30 段(
text/12-fm-function-return-types.txt:30,搜「accidentally returns」)。 ↩ -
出处:「Function return types」第 48–64 段(
text/12-fm-function-return-types.txt:60,搜「should not use the return value」)。 ↩ -
出处:「The never type」第 13–17 段(
text/14-fm-the-never-type.txt:15,搜「for a function that never returns in the」)。 ↩ -
出处:「The never type」第 71–75 段(
text/14-fm-the-never-type.txt:75,搜「unreachable」)。 ↩