编程语言研究书库

TypeScript 类型系统 — 从结构类型到 any、unknown 与组合类型

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:

let obj: { name: string } & { age: number } = { name: 'tom', age: 25 };
// obj 同时有 name 和 age 两个字段

并集类型用 |,意思相反——「或者」:null | { name: string } 表示 这个变量要么是 null,要么是那个对象,二选一,但不能同时13。 用并集之前必须先缩窄(narrow)类型:想读 .name,先写 if (unionObj !== null) 排除掉 null 那一支,编译器才放行13。

字面量类型把「值本身」变成类型:"tom" | "linda" | "jeff" | "sue" 表示这个变量只能取这四个字符串之一,赋个 john 就编译报错14。 它几乎总是和并集搭配——用类型表达「这个字段只允许这几个值」(枚举)的轻量写法。

类型别名(type)不给类型起新名字,只是给已有的类型一个更好用的名字:

type Points = 20 | 30 | 40 | 50;   // 长类型 → 短名字
let score: Points = 20;            // 99 会编译失败

别名可用于几乎任何类型:并集、交集、元组、函数、泛型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。

接下来是两个容易混的类型,分界线在「函数有没有跑完」:

  • void:函数跑完了,只是没返回有用的值(运行时实际返回 undefined)。 它是给调用方看的信号:别用这个返回值19。
  • never:函数根本没跑完——要么永远抛错,要么永远循环20。
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. 可带走的

  1. TypeScript 检查类型只看形状不看名字——结构类型 / duck typing。
  2. any 退出类型检查且会传染;项目里应开 noImplicitAny 把漏网的 any 变成错误。
  3. unknown 同样什么都能装,但用之前必须用类型守卫(如 instanceof)证明类型。
  4. & 交集 = 字段合并(取并集);| 并集 = 二选一;用并集前先缩窄。
  5. 字面量类型 "a" | "b" + 别名,就是「枚举合法值」的轻量写法。
  6. 函数类型写法 (参数) => 返回类型;类型里的参数名只是文档。
  7. void = 跑完但没值;never = 没跑完(抛错或死循环),编译器会标出其后代码不可达。

9. 原文地图

主题原书章原文位置
结构类型与 duck typingHow do types work?text/06-fm-how-do-types-work.txt:181(搜「structural typing」)
any 的定义与传染性The any typetext/07-fm-the-any-type.txt:5(搜「opts out」) · text/07-fm-the-any-type.txt:19(搜「silently disabling」)
noImplicitAnyThe any typetext/07-fm-the-any-type.txt:29(搜「noImplicitAny」)
unknown 的语义The unknown typetext/08-fm-the-unknown-type.txt:23(搜「cannot perform almost any operation」)
any 版编译通过、运行才炸The unknown typetext/08-fm-the-unknown-type.txt:195(搜「no errors」) · text/08-fm-the-unknown-type.txt:213(搜「never what we want」)
类型守卫与官方推荐The unknown typetext/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 typestext/09-fm-intersection-and-union-types.txt:11(搜「combining」) · text/09-fm-intersection-and-union-types.txt:15(搜「all the fields」)
并集与缩窄Intersection and union typestext/09-fm-intersection-and-union-types.txt:112(搜「let unionObj」) · text/09-fm-intersection-and-union-types.txt:138(搜「narrow」)
字面量类型Literal typestext/10-fm-literal-types.txt:5(搜「constrain」)
类型别名Type aliasestext/11-fm-type-aliases.txt:9(搜「another name」)
返回类型声明与 voidFunction return typestext/12-fm-function-return-types.txt:30(搜「readability」) · text/12-fm-function-return-types.txt:60(搜「should not use」)
函数类型签名Functions as typestext/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 typetext/14-fm-the-never-type.txt:15(搜「for a function that never returns in the」) · text/14-fm-the-never-type.txt:75(搜「unreachable」)

Footnotes

  1. 出处:「How do types work?」第 3–17 段(text/06-fm-how-do-types-work.txt:3,搜「JavaScript does have types」;:17,搜「count = "five"」)。 ↩ ↩2

  2. 出处:「How do types work?」第 177–181 段(text/06-fm-how-do-types-work.txt:179,搜「not concerned with its name」)。 ↩ ↩2 ↩3

  3. 出处:「How do types work?」第 45–52 段与第 48 段(text/06-fm-how-do-types-work.txt:48,搜「name: string」;:96,搜「does not complain」)。 ↩

  4. 出处:「The any type」第 5–17 段(text/07-fm-the-any-type.txt:5,搜「opts out」;:17,搜「no type」)。 ↩

  5. 出处:「The any type」第 37–49 段(text/07-fm-the-any-type.txt:39,搜「controlled by some other team」;:43,搜「escape hatch」)。 ↩

  6. 出处:「The any type」第 19 段(text/07-fm-the-any-type.txt:19,搜「silently disabling」)。 ↩

  7. 出处:「The unknown type」第 195–213 段(text/08-fm-the-unknown-type.txt:195,搜「succeeds with no errors」;:213,搜「never what we want」)。 ↩ ↩2

  8. 出处:「The any type」第 27–35 段(text/07-fm-the-any-type.txt:29,搜「noImplicitAny」)。 ↩

  9. 出处:「The any type」第 51–57 段(text/07-fm-the-any-type.txt:57,搜「no other way」)。 ↩

  10. 出处:「The unknown type」第 19–27 段(text/08-fm-the-unknown-type.txt:23,搜「cannot perform almost any operation」)。 ↩

  11. 出处:「The unknown type」第 59 段与第 289–293 段(text/08-fm-the-unknown-type.txt:59,搜「recommended by the TypeScript team」;:289,搜「type guards」)。 ↩ ↩2

  12. 出处:「Intersection and union types」第 11–17 段(text/09-fm-intersection-and-union-types.txt:11,搜「combining」;:15,搜「all the fields」)。 ↩

  13. 出处:「Intersection and union types」第 112–140 段(text/09-fm-intersection-and-union-types.txt:112,搜「let unionObj」;:138,搜「narrow」)。 ↩ ↩2

  14. 出处:「Literal types」第 5–11 段与第 5 段(text/10-fm-literal-types.txt:5,搜「constrain」;:19,搜「tom" | "linda」)。 ↩

  15. 出处:「Type aliases」第 9–11 段与第 63–67 段(text/11-fm-type-aliases.txt:9,搜「another name」;:65,搜「unions, intersections」)。 ↩

  16. 出处:「Functions as types」第 3–7 段(text/13-fm-functions-as-types.txt:3,搜「entire function signature」;:5,搜「first-class」)。 ↩

  17. 出处:「Functions as types」第 56–60 段(text/13-fm-functions-as-types.txt:60,搜「by position and type」)。 ↩

  18. 出处:「Function return types」第 28–30 段(text/12-fm-function-return-types.txt:30,搜「accidentally returns」)。 ↩

  19. 出处:「Function return types」第 48–64 段(text/12-fm-function-return-types.txt:60,搜「should not use the return value」)。 ↩

  20. 出处:「The never type」第 13–17 段(text/14-fm-the-never-type.txt:15,搜「for a function that never returns in the」)。 ↩

  21. 出处:「The never type」第 71–75 段(text/14-fm-the-never-type.txt:75,搜「unreachable」)。 ↩

On this page