编程语言研究书库

对象 — 交叉类型、interface 继承与工具类型

对象 — 交叉类型、interface 继承与工具类型

这一章讲三件事: 怎么把两个对象类型拼成一个(交叉类型 & 与 interface extends,以及为什么选后者); 键不固定的对象怎么保持类型安全(索引签名与 Record);以及四个内置「工具类型」 (Partial、Required、Pick、Omit)怎么对已有类型做增删改——包括它们一个著名的坑。 读完你会拥有对象类型的完整兵器架,以及 type 与 interface 之争的一个明确答案。

1. 顶层全景

  组合两个类型:          A & B(交叉)        interface B extends A(继承)
  键不固定的对象:        [key: string]: T(索引签名)   Record<K, T>
  对已有类型做变换:      Partial<T> 全可选 | Required<T> 全必填
                        Pick<T, K> 只留这些 | Omit<T, K> 去掉这些

  图说:三行分别是「拼」「兜」「改」。本章主线例子会把三行各走一遍。

读这张全景表时,先注意一个容易被「工具感」盖过的事实: 三行工具没有一行发明新的类型语义——交叉的属性合并、索引签名的任意键、 Partial 的可选化,全部是第 04 章对象类型语法的参数化复用。 本章的增量不是「新规则」,是「新写法」:把「拼形状、兜动态、做变换」这三件事 从「手写一遍」升级成「一行声明」。所以本章的正确读法是「工具速通」: 不必逐字记忆每个工具的语法(速查卡会兜着),要建立的是**「遇到这三类需求时, 知道有对应的一行写法」**的条件反射——剩下的,用的时候查卡即可。

2. 交叉类型:& 把两个形状叠在一起

解决什么问题: 已经有 Album(唱片)和 SalesData(销量)两个类型, 现在想要「一张唱片的销量数据」——五个属性全都要。复制粘贴五个属性太蠢,交叉类型一行解决:

type Album = {
  title: string;
  artist: string;
  releaseYear: number;
};

type SalesData = {
  unitsSold: number;
  revenue: number;
};

type AlbumSales = Album & SalesData;

const wishYouWereHereSales: AlbumSales = {
  title: "Wish You Were Here",
  artist: "Pink Floyd",
  releaseYear: 1975,
  unitsSold: 13000000,
  revenue: 65000000,
};

& 读作「与」:是 |(或)的反义词——并集说「要么这样要么那样」, 交叉说**「既是这个又是那个」**1。合出来的类型要求两个来源的全部属性, 少一个都不履约2。还可以三连:Album & SalesData & {genre: string}3。

并集与交叉的「方向感」

初学时 & 和 | 最容易混,一个判别抓手是结果的宽度:

  并集(A | B):结果比 A、B 各自都宽 —— 「更多种可能的值」
  交叉(A & B):结果比 A、B 各自都窄 —— 「更苛刻的要求,能过关的值更少」

  具体感受:能赋给 A & B 的值,必须同时满足两个类型的全部要求——
  要求叠要求,合格者自然变少;能赋给 A | B 的值,满足任何一个就行——
  门槛低,合格的多。

  图说:「与」收窄(值要的东西更多),「或」放宽(值要的东西更少)。
  记反了,一切联合/交叉的推理都会反——这个方向感是全章的地基。

这个方向感在第 13 章还会再受一次检验:那里会见到「函数联合收成交集」的 怪癖——看似矛盾,实则正是「参数方向的宽窄与返回值方向相反」的一次重演。 地基打正,后面的反常都能追回家。

还有一个「方向感」的日常应用:读别人的交叉/联合类型时,先判断它声明的是 「值的集合」还是「要求的清单」。Config | Defaults 读作「配置可以来自 任何一边」(值的两条路);Config & Validated 读作「配置必须同时满足两边」 (要求的两道关)。把两类读法混用,是联合类型误用率常年冠军的常见成因—— 第 06 章会完整展开这条战线。

交叉的适用域:& 与 extends 的分工预告

交叉和 interface extends 干的是同一件事(把形状合并),但适用域不同, 值得在使用前先划清:

  交叉(&)的主场:
    · 合并两个「现成的」type 别名(别名不能被 extends)
    · 临时拼装:函数参数处写 (A & B),用完即弃
    · 叠加修饰:Base & { id: string } 这类「基础 + 补丁」
  interface extends 的主场:
    · 有层级语义的继承(子类型是父类型的一种,第 09 章展开)
    · 需要声明合并(同名接口自动合流)的场景
    · 报错时机和性能有要求的对象(见下一节两个硬理由)

  图说:& 是「平级拼装」,extends 是「层级派生」;
  平级拼装用完即弃,层级派生会被子类型、实现类持续引用。

这条分工也解释了下一节的原书裁决:对象类型的「主户口」放 interface, 交叉留给「拼装现场」。两者不是竞争关系,是一个负责存档、一个负责施工。

交叉的暗面:冲突收编成 never

交叉两个对象类型很美好;交叉不相容的东西,结果会变丑。

type StringAndNumber = string & number;   // 结果:never

字符串和数字没有共同值——没有东西「既是字符串又是数字」,所以结果是 never(第 06 章的「不可能」)4。 对象类型也一样,只是更隐蔽:

type User1 = {age: number};
type User2 = {age: string};
type User = User1 & User2;
// 悬停 User → { age: never }

age 不可能既是 number 又是 string,于是被解析成 never5。 记住这个症状的的样子:交叉的结果里冒出 never,说明两个来源在同一个属性上打架。 诊断信息不吵不闹,但属性实际上永远没法被满足——这个坑下一节就有对症的药。

never 的三步排查法:交叉冒出的 never 从哪来

交叉结果里出现 never,排查可以按固定顺序走三步:

  第 ① 步:悬停交叉结果的展开形状,找到 never 所在的属性名
  第 ② 步:回到两个(或多个)来源类型,分别看那个属性声明成了什么
  第 ③ 步:冲突定了——决定哪一边是对的(或两个来源本就不该交叉)

  图说:三步全是悬停,没有猜。never 是「值的不可能」,不可能必有两处声明在打架;
  找到打架双方,冲突的性质(类型不同?可选性不同?字面量互斥?)一目了然。

冲突的「性质」决定修法,常见的三种和各自的处方:

冲突性质例子处方
类型不同age: number vs age: string二选一;或改并集(number | string)
可选性不同bio?: string vs bio: string明确要不要必填,两边对齐
字面量互斥status: "a" vs status: "b"通常意味着两个来源不该交叉,该用判别联合(第 06 章)

第三种尤其值得警惕:字面量互斥的冲突,常常说明你把「或」写成了「与」—— 本想要「状态是 a 或 b 的对象」,交叉却要求「同时是 a 和 b」。 此时正确结构是第 06 章的判别联合,不是调和交叉。这个「或/与混淆」是交叉类型 最常见的误用根源。

3. interface 与 extends:面向继承的另一半拼图

解决什么问题: type 语法不是定义对象类型的唯一方式。TypeScript 从第一个版本起就有 interface(接口),语法上只差在:没有等号,关键字换名6:

type Album = {
  title: string;
  artist: string;
  releaseYear: number;
};

interface Album {
  title: string;
  artist: string;
  releaseYear: number;
}

看着像同义词,其实差别很大。interface 的看家本领是 extends——声明继承关系:

interface Album {
  title: string;
  artist: string;
  releaseYear: number;
}

interface StudioAlbum extends Album {
  studio: string;    // 录音棚
  producer: string;  // 制作人
}

interface LiveAlbum extends Album {
  concertVenue: string;
  concertDate: Date;
}

StudioAlbum 自动拥有 Album 的三个属性,再加自己的两个7。 americanBeauty 和 oneFromTheVault 两个实例摆在一起,继承关系一眼可读8。 和交叉一样,extends 也能多继承——逗号分隔即可:

interface BonusConcertEdition extends StudioAlbum, LiveAlbum {
  numberOfDiscs: number;
}

多继承这里有一条「菱形问题」的轻量版值得预防:两个父接口若都带同名属性, extends 会当场拒绝(冲突在定义处现形,正是它比交叉强的地方之一)。 设计多继承时,小接口的命名要带语义(WithId、WithTimestamps), 让「同名冲突」在接口命名阶段就暴露为「语义重复」,而不是留到合并时。

& 还是 extends?原书给了两个硬理由,都选 extends

同一个需求(让 B 拥有 A 的属性)有两条语法,原书的裁决相当果断:能用 interface extends 就用, 理由有两个9。

理由一:冲突报错更早、更准。 回想上一节那个 age: number 和 age: string 的冲突—— 交叉类型把它静默解析成 never,要等到你使用 age 时才报错; interface extends 在定义处就拒绝:

interface User1 {
  age: number;
}

interface User extends User1 {
  age: string;  // 红线:Interface 'User' incorrectly extends interface 'User1'.
}               //      Types of property 'age' are incompatible.

错误的落点从「使用时」提前到「定义时」——型错了在型的门口被拦,而不是等值来了才炸10。

理由二:性能。 交叉类型每次使用都重新计算;interface 的结果可以按名字缓存, 算一次、处处复用。在大项目里,这个差别直接体现在 IDE 的流畅度和 tsc 的检查时间上11。

原书的结论句值得原文记住:「using interface extends is better for catching errors and for TypeScript performance」——更好的错误、更好的性能9。

两个硬理由各自值得再各推一层:

  理由一(报错时机)的工程含义:
    「定义处报错」的价值不只是提前——它把错误的责任归位了。
    交叉类型把冲突拖成 never,等于宣布「这个属性自此作废」,
    但「作废」是谁的决定?没人决定,是合并机制的副作用。
    interface extends 在定义处拒绝,等于宣布「这个组合不合法,谁写的谁改」。
    责任可归属,是报错时机提前之后真正的红利。

  理由二(性能)的判断法:
    「每次使用都重算」的代价,和类型的使用频率成正比。
    一个交叉类型只在一处用,性能差异是零;被 50 个函数引用,
    差异就变成 50 次重复计算。判断标准:这个合并结果的**引用次数**。
    引用次数 ≥ 2 就该给它名字(而命名 = interface 的势力范围),
    这其实是同一个理由的两种说法:缓存的前提是命名。

  图说:两个理由合流成一条——「值得重复引用的形状,给它名字和户口(interface);
  一次性拼装的,留在现场(&)」。

那为什么不全用 interface?type 与 interface 的对照

interface 只擅长对象和函数;type 别名可以命名任何东西——并集、交叉、原始类型,随你12:

type Union = string | number;  // interface 写不出这个

interface 还有一个著名的怪癖:声明合并(declaration merging)。 同一个作用域里两个同名 interface 会被自动合并成一个:

interface Album {
  title: string;
  artist: string;
}

interface Album {
  releaseYear: number;
  genres: string[];
}

// 实际生效(两个声明被合并):
interface Album {
  title: string;
  artist: string;
  releaseYear: number;
  genres: string[];
}

同名 type 则直接报错(duplicate identifier)——「重复定义」这个失误被当场抓住13。

听起来像特性?原书坦白得很:「我们人生的好几个小时,丢在了同一个 2000+ 行的文件里 两个同名 interface 上。」(We have lost hours of our lives...)—— 在 JavaScript 的世界里这行为相当反常,是个 gotcha14。它存在的好理由要到第 10 章 (给第三方库的类型补丁)才揭晓,平时它更多是陷阱。

这个「gotcha 的正当用途在四章之后」的结构,值得作为读书方法单独留一句: **本章有三处「先埋后收」——声明合并(第 10 章)、索引签名的字面量限制(第 16 章)、 DistributiveOmit 的 extends(第 16 章)。**读到时不要急着问「为什么不能更聪明」, 记下「此处有后文」即可;全书的章节是按「先给手感、后给机制」排布的, 很多「限制」的真相都在后面等语境齐了再揭盅。这也是原书与工具文档的差别: 文档解释每个特性,教材安排每个特性的出场时机。

所以原书给出了自己的默认(也是社区争论多年的答案):

简单对象类型默认用 type;需要 extends 时再换 interface。 type 更灵活,还不会在深夜偷偷合并你的声明。但这是个接近的抉择—— 从面向对象语言过来的人偏爱 interface,也完全可以理解15。

判断(我们的,不是书里的): 「默认 type、要 extends 再换 interface」是 2026 年社区的主流答案, 但值得想清楚它成立的条件:它默认你更看重灵活性(并集、交叉、模板字面量都只有 type 玩得转) 而不是多人协作下的继承体系。在严格分层的大型后端代码库里,interface 的声明合并 反而是给横切关注打补丁的正规通道(第 10 章)。「默认 type」不是法律,是风格。 如果错,会错在: TypeScript 若在未来版本给 type 也加上受控的合并能力(社区确实反复讨论过), interface 的最后一块专属领地将消失,这个「默认」会更没有争议——但眼下(2026)原书的建议站得住。

4. 动态键:键名事先不知道的对象

解决什么问题: JavaScript 里最常见的动作之一:先有空对象,再往上面动态挂键。

// JavaScript:毫无问题
const albumAwards = {};
albumAwards.Grammy = true;
albumAwards.MercuryPrize = false;

同样的代码在 TypeScript 里三连报错:Property 'Grammy' does not exist on type '{}'16。 你可能会不服气:编译器不是会收窄吗,看不出我在加键?它看得出,但它选择保守—— 随便允许往对象上加键,等于放弃拼写检查(第 02 章那条「opinionated 规则」的又一体现)。 想动态加键,得先声明「我允许」。

索引签名:先声明「任意字符串键都行」

术语先落地:往 albumAwards.Grammy 里写值,这个动作叫 indexing(索引)—— 用字符串键 Grammy 索引进对象17。对应的类型语法叫索引签名(index signature):

const albumAwards: {
  [index: string]: boolean;
} = {};

albumAwards.Grammy = true;       // ✅
albumAwards.MercuryPrize = false; // ✅

读法:[index: string]: boolean = 「用任何字符串键索引进来,拿到的都是 boolean」18。 方括号里的名字只是描述,写成 [iCanBeAnything: string] 也一样19。 interface 和 type 里同样能用(interface AlbumAwards { [index: string]: boolean })20。

索引签名的三个边界,用之前先知道

索引签名「任意字符串键都行」的代价,值得在上手前就看全:

  边界一:值类型必须统一
    [index: string]: boolean 意味着「每个键的值都是 boolean」,
    一旦混进 { Grammy: true, year: 1965 },year 报错——
    想混装多种值,值得是 unknown 再收窄,或拆成两个对象。

  边界二:已知键失去了「必填」保障
    对象类型 { Grammy: boolean } 说「Grammy 必须存在」;
    索引签名 { [index: string]: boolean } 说「Grammy 可以存在」——
    键的存在性从合同降级成可能。需要「某些键必有 + 其余随意」的组合写法,
    见下一节的「已知键 + 动态键」。

  边界三:键集合不可枚举
    索引签名类型的 keyof 是 string——你拿不到「实际有哪些键」的清单
    (第 11 章的 keyof 教训)。需要可枚举键集时,Record + 字面量并集键
    或 as const 对象才是正解。

  图说:索引签名买的是「接纳任意键」,付的是「值的同质性、键的必然性、
  键的可枚举性」——三样代价明码标价。

这三个边界决定了索引签名的适用域:键集合开放、值类型单一、不关心枚举的场景 (缓存、字典、计分板)。超出这个三角任何一条,都该考虑 Record 的窄键版 或「已知键 + 动态键」的组合写法——这正是后面两节的内容。

Record 是内置工具类型,两个类型参数:键的类型、值的类型。

const albumAwards: Record<string, boolean> = {};
albumAwards.Grammy = true;

和索引签名等价,但更短21。关键差异在键可以是字面量并集——索引签名做不到:

const albumAwards: Record<"Grammy" | "MercuryPrize" | "Billboard", boolean> = {
  Grammy: true,
  MercuryPrize: false,
  Billboard: true,
};

// 索引签名这么写会报错:
{
  [index: "Grammy" | "MercuryPrize" | "Billboard"]: boolean;
}
// An index signature parameter type cannot be a literal type or generic type.
// Consider using a mapped object type instead.

报错信息本身就是教材:索引签名不收字面量类型,并提示「用映射对象类型」—— 那是第 16 章的内容,这个限制的底层原因到那时才揭盅22。

原书对 Record 的定位干脆利落:「动态键的首选」(go-to)——它灵活、可读、可复现23。

Record 还有两个速记价值值得点名。其一,并集键的 Record 是最快的「全必填」声明: Record<"Grammy" | "MercuryPrize" | "Billboard", boolean> 一行等于三个必填属性的 对象类型——写配置表、枚举映射时,比手写接口短一半,而且键清单能复用(第 11 章的 keyof typeof 会从这种对象里反推出键联合)。其二,Record<K, unknown> 是 「形状未知的对象」的诚实标注:比 any 多一层「它至少是个对象」的承诺, 比具体类型少一份「我都知道」的虚张声势——在边界数据(第 05 章的 JSON.parse 一系) 上,它是 any 和精确类型之间的中间档。

已知键 + 动态键的组合

真实需求经常是混合的:已知键要有(可能必填),未知键也要放行。两种等价的写法:

// type 版:Record 已知并集 ∩ 索引签名
type BaseAwards = "Grammy" | "MercuryPrize" | "Billboard";
type ExtendedAlbumAwards = Record<BaseAwards, boolean> & {
  [award: string]: boolean;
};

// interface 版:extends 已知键 + 索引签名
interface BaseAwards {
  Grammy: boolean;
  MercuryPrize: boolean;
  Billboard: boolean;
}
interface ExtendedAlbumAwards extends BaseAwards {
  [award: string]: boolean;
}

两种都能让 {Grammy: true, ..., "American Music Awards": true} 合法—— 已知的三个键必填(并集做键的 Record 默认全必填),其余任意字符串键放行24。 原书在两者间再次选了 interface extends:与前面同一套理由(报错时机 + 性能)25。

组合写法的代价:双重语义重叠时的边界情况

「已知键 + 动态键」的写法威力大,但有一个值得知道的边界: 已知键的类型必须和动态键的值类型兼容,否则冲突:

interface ExtendedAwards {
  Grammy: boolean;              // 已知键
  [award: string]: number;      // 动态键说「所有字符串键的值都是 number」
}
// ❌ Grammy 的 boolean 和索引签名的 number 冲突

索引签名是「所有字符串键」的宣言——已知键既然也是字符串键,就必须服从 索引签名的值类型。想要「已知键 boolean、动态键 number」的混搭, 索引签名做不到(这正是索引签名「值类型必须统一」边界的回声); 能做到的是「已知键更具体、动态键是其父类型」的层级关系:

interface ExtendedAwards {
  Grammy: "yes" | "no";        // 更具体的字面量
  [award: string]: string;      // 动态键宽,兼容具体键
}
// ✅ 已知键的类型是动态键值类型的子集

这个「子集兼容」规则值得记住:组合写法里,已知键的类型必须能塞进 动态键的值类型里——顺序反过来(动态键比已知键更具体)必然报错。 排障「interface extends 后索引签名报错」时,先看这两个声明的值类型是否 父子颠倒。

PropertyKey 是全局类型,代表所有能当键的东西:

// 来自 TypeScript 自带的 lib.es5.d.ts
declare type PropertyKey = string | number | symbol;

键的三种合法类型(字符串、数字、符号)的并集26。它最大的用武之地是给「键」这个参数定型。 练习里的 hasKey 函数:参数 key 从 string 改成 PropertyKey, 字符串键、数字键、符号键的测试就全过了——比手写 string | number | symbol 短,而且语义更准: 「任何合法键」27。看到对象的键类型想写 string | number | symbol,写 PropertyKey。

还有一个值得留意的细节:PropertyKey 的定义不在这本书里,而在 TypeScript 自带的类型文件 lib.es5.d.ts 里——就是那个 declare type PropertyKey = string | number | symbol;26。 「declare」打头的文件是声明文件(.d.ts),内容只有类型、没有实现,整个 JavaScript 世界(包括 Object.hasOwnProperty 的参数类型)都是靠它们才有了类型。 这些文件从哪来、怎么给项目配上,是第 14 章的专题;这里先埋一个印象: 你日常用到的每一个全局类型(Promise、Array、PropertyKey……)都有出处,都住在声明文件里。 悬停追进去看定义,是学习类型系统最被低估的习惯之一。

判断(我们的,不是书里的): 动态键三件套(索引签名 / Record / PropertyKey)是「渐进类型」哲学的好样本: JavaScript 的惯用法(动态挂键)不被禁止,但每一招都要求你显式声明边界—— 键是什么类型、值是什么类型。类型系统的角色是给动态性画跑道,不是拆掉跑道。 如果错,会错在: 对于键极其发散的对象(几百个键的配置表),跑道的维护成本可能超过收益, 这时 Record<string, unknown> 加 Zod 校验(第 06 章的动线)反而更诚实—— 「我控制不了键的集合,但我校验值」。

5. 工具类型:对已有类型做变换

组合与动态键解决「从哪来」;这一节解决「怎么改」。 四个内置工具类型,名字即用法:Partial 全可选、Required 全必填、 Pick 挑几个留下、Omit 挑几个删掉28。

在逐个讲之前,先把四个工具放进一张「语义坐标」——它们的全部差异, 可以压缩成两个维度:可选性(改修饰符)vs 键集(改成员)、 加宽(放开)vs 收窄(收拢):

                改可选性          改键集
  加宽(放开)    Partial          (Omit 删键 = 键集收窄…)
  收窄(收拢)    Required         Pick(留) / Omit(删)

  图说:四个工具两两对仗——Partial/Required 是同一操作的两个方向,
  Pick/Omit 也是。学一个会一对,这就是它们被设计在一组的原因。

这个坐标还有一个实用推论:「改可选性」的工具天然分配(只动修饰符), 「改键集」的工具天然不分配(要合并键)——上一节的分配性之谜, 在这里有了结构性的解释。不需要背哪个工具分配,记住维度就够了: 动修饰符的,逐成员来;动键的,先合并。

主线例子继续用 Album。四个工具轮流上:

Partial:更新场景的全可选

interface Album {
  title: string;
  artist: string;
  releaseYear: number;
  genre: string;
}

type PartialAlbum = Partial<Album>;
// { title?: string; artist?: string; releaseYear?: number; genre?: string; }

const updateAlbum = (album: PartialAlbum) => { /* ... */ };

updateAlbum({title: "Geogaddi", artist: "Boards of Canada"}); // 只更新两个字段,合法

「更新」语义天然是部分的:传进来的对象只含要改的字段。 Partial 把每个属性都加上 ?,完美对位29。

把 Partial 放进「更新流」的完整上下文,它的设计意图更清楚:

  典型更新链路,三个类型各司其职:
    Album            → 完整模型(单一事实源,来自第 04 章)
    Partial<Album>   → 更新载荷(调用方:我只想改这几样)
    Required<Album>  → 落库前校验(数据层:字段必须齐全)

  图说:模型在链条两头的形态由 Partial/Required 这对镜像工具收尾,
  中间任何一环想「少传」或「缺字段」,类型层面立刻有对应工具表态。

这个三件套视角也解释了为什么 Partial 和 Required 互为镜像却不同源: Partial 服务的方向是入口放宽(调用方自由),Required 服务的方向是 出口收紧(存储安全)。一个方向一个工具,合起来覆盖「数据从进到出」的 两种形态要求。练习 6-8(updateProduct 的部分更新)正是这个三件套的中段应用: Partial<Omit<Product, "id">> —— Omit 把 id 从更新载荷中剔除(身份不能改), Partial 把剩下的全部放宽(改哪样随意)30。

Required:反向操作,且只有一层

type RequiredAlbum = Required<Album>; // 所有 ? 全部摘掉

重要的边界:Partial 和 Required 都只作用一层。 genre 如果是个嵌套对象,Required<Album> 只会让 genre 本身必填, genre 里面的 parentGenre、subGenre 依然可选——悬停看得清清楚楚31。 要深度版本,原书指路 type-fest 库(并附了 GitHub 链接)32。

「只一层」这个边界值得和第 08 章的 Readonly 对照着记—— Readonly<T> 同样只锁一层,ReadonlyArray 同样只锁数组本身。 「工具类型默认浅层」是 TypeScript 内置工具的系统性格,原因也很一致: 深版本必须递归,递归在类型层面意味着潜在的无穷展开, 内置工具选择了最安全的形态。需要深版本时的三条路(按成本排): 手写递归映射类型(第 16 章的技能)、type-fest 这类工具库、 或者重新设计数据让深度不需要(扁平化 often 是最优解)。 多数项目走到第二条就停了;第三条常被忽视,却常常最对—— 「需要深度工具」有时只是「嵌套太深」的症状。

Pick 与 Omit:留和删

type AlbumData = Pick<Album, "title" | "artist">;      // 只留这两个
type AlbumData2 = Omit<Album, "id" | "releaseYear" | "genre">; // 删掉这三个,殊途同归

选型有讲究,原书用一句话说透:

Omit 的对象随源类型长大;Pick 的对象保持不变。 Album 以后加了新属性,Omit<Album, "id"> 自动包含它;Pick<Album, "title"|"artist"> 不会33。

「创建还没有 id 的对象」(比如待入库的新记录)是 Omit 的经典场景:Omit<Album, "id">34。

Omit 的怪癖:删掉不存在的键不报错

type AlbumWithoutProducer = Omit<Album, "producer">; // Album 没有 producer 键!但不报错

Pick 则严格得多——挑一个不存在的键,当场报错: Type '"producer"' does not satisfy the constraint 'keyof Album'35。

一松一严不是疏忽,是当年刻意的决定:TypeScript 团队实现 Omit 时在「严格版 / 宽松版」之间选了宽松版—— 社区当时更流行宽松版;又因为 Omit 是全局类型、人人可用, 宽松版「更兼容、更少意外错误」36。原书附了当年讨论的 GitHub issue 和 PR 链接, 并提醒:宽松的 Omit 多数时候够用,只是报错的方式可能出乎你意料; 严格的版本,第 16 章教你手写37。

联合类型上的大坑:Omit/Pick 不「分配」

这是本章技术上最要紧的一段。三个类型并成一个联合,想统一删掉 id:

type Album = { id: string; title: string; genre: string };
type CollectorEdition = { id: string; title: string; limitedEditionFeatures: string[] };
type DigitalRelease = { id: string; title: string; digitalFormat: string };

type MusicProduct = Album | CollectorEdition | DigitalRelease;
type MusicProductWithoutId = Omit<MusicProduct, "id">;

你期待的是「三个成员各自删 id 的新联合」;实际悬停一看:

// 实际得到:
type MusicProductWithoutId = {
  title: string;
};

只剩 title!Omit 先把联合拍平成公共属性(id 和 title 是三个成员仅有的公共属性), 再删掉 id38。对比一下:Partial 和 Required 在联合上行为正常—— 逐个成员包一层,联合结构原样保留39。

病根一句话:Omit 和 Pick 不具有分配性(not distributive)—— 遇到联合,它们不逐个成员操作,而是把成员揉成一体再动手40。

药方是用条件类型手写一个分配版(第 16 章会讲 T extends any ? ... : never 就是分配开关):

type DistributiveOmit<T, K extends PropertyKey> = T extends any
  ? Omit<T, K>
  : never;

type MusicProductWithoutId = DistributiveOmit<MusicProduct, "id">;
// ✅ 三个成员各自删掉 id,联合结构完整保留

DistributivePick 同理(K extends keyof T)41。结论: 在联合类型上用 Omit/Pick,需要分配版,否则结果出乎意料42。

这条知识在 2026 年的真实项目里极其高频——判别联合(第 06 章)恰好是最爱用联合的类型形态, 而「给判别联合统一删个 id」又是高频操作。原书把 Omit 的两个坑(宽松键、不分配)放在同一章, 是因为这两处恰好是 Omit 最容易咬人、而报错最不显眼的位置——一个静默放过,一个静默拍平。

分配性的判据:工具会不会「逐个成员操作」

什么工具分配、什么不分配,有一个可记的判据:

  分配的本质 = 对联合的每个成员各做一次操作,再把结果合回联合。
    Partial<T>      ✅ 分配:Partial<A|B> = Partial<A> | Partial<B>
    Required<T>     ✅ 分配(同上,逐成员)
    Pick/Omit       ❌ 不分配:先拍平公共属性,再统一操作
    Record<K, V>    N/A(键参数,不操作联合成员)

  图说:Partial/Required 是「逐件加工」,Pick/Omit 是「混装处理」——
  同一箱水果,逐件擦和倒在一起搅拌,出品完全不同。

为什么 Pick/Omit 不分配?原书没展开,补一句机制层面的解释(补充,来自通用知识): 它们的实现是「把 T 先转成一个完整的键值映射再挑选/删除」—— 转映射这一步对联合而言就是「合并公共键」的操作,成员边界在这一步已经消失。 Partial/Required 则只动属性的可选修饰符,不需要转映射,成员边界得以保留。 判断一个新工具分不分配,就看它的实现需不需要「先合并成员」—— 需要合并的,都不分配。这也是第 16 章的「分配性开关」(T extends any ? ... : never) 存在的理由:它强制让每个成员单独过一遍操作,绕过合并。 等于给工具类型划清了「哪里别信它」的边界。

四个工具类型速查(本章的口袋卡片)

工具一句话典型场景本章揭露的边界
Partial<T>全属性加 ?部分更新(updateAlbum)只作用一层,不递归31
Required<T>全属性摘 ?补齐可选对象为完整对象同上,只一层31
Pick<T, K>只留 K 这些键对外契约、响应投影键不存在会报错;对联合不分配3540
Omit<T, K>删掉 K 这些键去 id 的新建对象删不存在的键不报错;对联合不分配3640

速查表之外还有一条贯穿本章的元规则值得单独记住: 工具类型永远不会修改源类型——Partial<Album> 是一个新类型,Album 自己毫发无损。 所有变换都是「派生新类型」,这正是「单一事实源」能成立的原因: 源头只有 Album 一份,所有变体都是它的纯函数,改源头、变体自动跟随。 第 11 章会把这条元规则推到极限——用 keyof、typeof 让派生连「键名清单」都不用手写。

Pick 和 Omit 的生长方向:源变长时,谁跟着变

练习 6-7 的收尾给了速查表之外的一个维度,值得单独立一条: Pick 和 Omit 在「源头加字段」时行为相反:

  interface User { id; name; email; role; }
  需要「只有 name 和 email」的返回类型:

  Pick<User, "name" | "email">   → 源加了 phone 字段 → 返回类型不变(还是两项)
  Omit<User, "id" | "role">      → 源加了 phone 字段 → 返回类型自动多一个 phone
  interface extends 拆分        → 同 Pick:NameAndEmail 固定两项

  图说:Pick 是「白名单」——源再怎么长,我只认这几个;
  Omit 是「黑名单」——源长出来的新东西,自动进我的集合。

原书给的选择依据不是「哪个更高级」,而是需求语义: 「这个接口就是这两个字段」(对外契约)选 Pick 或 extends 拆分——新字段不该自动混入; 「这个接口是『除了某几个之外的全部』」(排除敏感字段)选 Omit——新字段应该自动跟来43。 选错的代价也有方向性:该用 Pick 用了 Omit,契约会在源扩张时悄悄变宽; 该用 Omit 用了 Pick,新字段永远漏不出去。两个方向的事故都不报错—— 这是工具选型里少数「没有编译器兜底、全靠语义判断」的决策点,值得在团队里统一口径。

6. 组合技:工具类型的套娃

练习里的两道题展示了工具类型如何组合,正好收束主线例子:

// 更新产品:去掉 id,且所有字段可选
const updateProduct = (
  id: number,
  productInfo: Partial<Omit<Product, "id">>,
) => { /* ... */ };

读法从内向外:先 Omit<Product, "id">(更新的不该带主键),再 Partial<...>(改哪几个随你)。 调用方只传 {name: "Book"}、{price: 12.99} 或任意组合,全部合法44。

另一道题是 Pick/Omit/interface 三条路线各解一遍「fetchUser 只返回 name 和 email」, 并点破三者的成长方向:Omit 版随 User 长大,Pick 版和 interface 版保持不变—— 选哪条,取决于你希望这个类型跟着源类型演化,还是冻结45。

套娃的读法与写法纪律

组合技一旦开始套娃,读写两端都需要一点纪律:

  读(遇到别人的套娃):
    从内向外逐层剥——Partial<Omit<Product, "id">> 先想「去掉 id 的 Product」,
    再想「全可选版的它」。每层停在悬停上验证一次,别一口气读到最外层。

  写(自己制造套娃):
    从外向内设计——先定「这个参数的最终语义是什么」(部分更新的载荷),
    再选最后一步的工具(Partial),往里填它的输入(Omit<Product, "id">)。
    两层是舒适区,三层以上请拆中间类型。

  图说:读和内向外,写和外向内——方向相反,都符合各自的心智负荷。

这条读写纪律和第 11 章「套娃超过两层就拆中间类型」完全同构—— 类型表达式的复杂度管理和值表达式一模一样,只是类型没有运行时性能压力, 更容易被写成「炫技式长表达式」。团队 code review 时见到三层以上套娃, 第一个问题该是「为什么不拆」,而不是「它算得对不对」。

样本走查一:BaseEntity 的两代写法(交叉 → extends)

练习 6-1/6-2 是同一次重构的前后两半,把「为什么 extends 更好」演成了活剧。 起点:User 和 Product 都各自带着 id、createdAt 两个字段,重复了。

第一代(交叉): 抽出 BaseEntity,用 & 叠回去:

type BaseEntity = {
  id: string;
  createdAt: Date;
};

type User = {
  name: string;
  email: string;
} & BaseEntity;

type Product = {
  name: string;
  price: number;
} & BaseEntity;

行为正确,重复消失46。

第二代(extends): 换成 interface 体系:

interface BaseEntity {
  id: string;
  createdAt: Date;
}

interface User extends BaseEntity {
  name: string;
  email: string;
}

interface Product extends BaseEntity {
  name: string;
  price: number;
}

结构相同,但报错时机和性能都换了档(第 3 节的两条理由)。 练习的附加题还展示了多继承的拆法:把 id 和 createdAt 再拆成 WithId、WithCreatedAt 两个小接口,interface User extends WithId, WithCreatedAt—— 「你的 TypeScript 编译器会感谢你的」(原书原话)47。

这个拆法的价值在于语义粒度:不是每个带 id 的东西都带 createdAt。 小接口像乐高积木,组合而非堆积——这是 interface 体系真正的用法, 而不是「把大类型切成碎片的强迫行为」。

两代写法的「悬停对比实验」

两代写法的差异,最直观的验证方式是同一次悬停:

  悬停第一代(交叉)的 User:
    → 显示 A & B 的展开形状(属性全在,但交叉结构可见)
  悬停第二代的 User:
    → 显示扁平的成员列表,BaseEntity 的存在被「消化」了

  再看 IDE 行为:
    交叉版:Go to Definition 跳到交叉表达式,要再跳一次才到 BaseEntity
    extends 版:直接跳到 interface User,成员的来源一眼分清

  图说:extends 把「继承」消化进类型的身份,交叉把「组合」留在类型的表面——
  这个差别平时无害,调试和重构时天天有感。

这个实验也是「报错时机」差异的体感来源:交叉版的冲突要等使用才炸, extends 版的冲突在定义处就拒——两代写法跑同一段错误代码, 红线的位置差着十万八千里(第 3 节的核心理由,在此有了可触摸的形态)。 做题时如果两代写法都写了,这个悬停对比值得亲手做一遍—— 一分钟的实验,胜过十遍「报错更早」的抽象陈述。

样本走查二:用 Record 给键上锁(配置表)

练习 6-5 是 Record 的典型战场:环境配置表只允许 development / production / staging 三个键。

type Environment = "development" | "production" | "staging";
type Configurations = unknown; // 起点:完全没有检查

直觉的解法 Record<string, {apiBaseUrl: string; timeout: number}> 是错的—— 键是 string,意味着任何键都合法,notAllowed 照样放行48。 正解是把键的并集喂给 Record:

type Configurations = Record<
  Environment,
  {
    apiBaseUrl: string;
    timeout: number;
  }
>;

键的类型从「任意字符串」收紧到「三个字面量」, notAllowed 键从此在类型层面不存在49。 这道题还演示了 @ts-expect-error 的正经用法:练习里 notAllowed 上面就标着它—— 类型收紧到位后,这行「预期报错」真正报错了,红线反而消失(预期兑现); 类型没收紧时,该报错没报,@ts-expect-error 自己亮红线50。 这是把测试思维搬进类型层的绝佳样本:断言「这件事应该报错」,和断言 1+1=2 是同构的。

样本走查三:已知键必填 + 动态键放行(成绩表)

练习 6-4 把第 4 节的组合技落到一个具体形状:成绩单必须有数学、英语、科学三门, 但还要允许任意加别的科目。

interface Scores extends RequiredScores {
  [key: string]: number;
}

interface RequiredScores {
  math: number;
  english: number;
  science: number;
}

或者单接口版:{ [subject: string]: number; math: number; english: number; science: number; }—— 两者功能等价;extends 版多一个可以单独复用的 RequiredScores51。 注意一个细节:这个场景里 @ts-expect-error 断言的是「漏掉 science 应该报错」—— 类型写对后,断言兑现、红线消失。同一个指令,既能断言「禁止的东西被禁」, 也能断言「必须的东西必填」,方向正好相反52。

样本走查四:fetchUser 的三条路线(演化方向的选择题)

练习 6-7 把 Pick/Omit/interface 的成长差异做成了一道选择题,值得完整走一遍。 User 接口有四个字段(id、name、email、role),而 fetchUser 的调用方只应拿到 name 和 email。

路线 A:Pick——显式挑选,结果冻结。

type PickedUser = Pick<User, "name" | "email">;

const fetchUser = async (): Promise<PickedUser> => {
  const response = await fetch("/api/user");
  const user = await response.json();
  return user;
};

将来 User 加了 avatar 字段,PickedUser 纹丝不动——它只承诺 name 和 email。 对 API 响应类型,这通常是正确姿态:对外契约不该随内部模型漂移53。

路线 B:Omit——显式排除,结果随源长大。

type OmittedUser = Omit<User, "id" | "role">;

User 新增字段自动流入 OmittedUser。省心,但契约变得含糊: 「id 和 role 之外的一切」——将来 User 加了 internalNotes 这种内部字段, 它也会跟着漏出去。用 Omit 声明对外类型,等于把未来的字段决定权交给了源模型54。

路线 C:interface 组合——冻结,且可复用。

interface NameAndEmail {
  name: string;
  email: string;
}

interface User extends NameAndEmail {
  id: string;
  role: string;
}

const fetchUser = async (): Promise<NameAndEmail> => { /* ... */ };

效果和 Pick 一样冻结,但 NameAndEmail 本身成了一个可复用的公开契约—— 别的地方要表达「姓名加邮箱」时不必重复定义55。

原书对三条路线的总结只有一句,但信息量很大: 「用 Omit 意味着对象随源长大;用 Pick 和 interface extends 意味着对象保持不变。 按你的需求选。」56这道题没有标准答案,有的是一个清晰的决策维度: 这个类型是「内部模型的投影」(选 Omit)还是「独立契约」(选 Pick/interface)?

样本走查五:scores 的一题四解(动态键的四种写法)

练习 6-3 要求给 scores.math = 95 这类动态键定型,标准答案有四个,恰好是第 4 节工具的目录:

// ① 内联索引签名
const scores: { [key: string]: number } = {};

// ② interface
interface Scores { [key: string]: number }
const scores: Scores = {};

// ③ type 别名
type Scores = { [key: string]: number };
const scores: Scores = {};

// ④ Record
const scores: Record<string, number> = {};

四个都合法,选哪个看场景:一次性使用选①;要复用选②③(此时再叠上第 3 节的「默认 type」偏好); 要键是字面量并集、或想要最短的写法,选④57。这一题四解的设计本身就在教学: 工具没有高下,只有对口与否。

五个样本走完:一条完整的选型决策线

把五个样本走查串成线,本章的选型逻辑就从头到底走通了一遍:

  需求线索                    首选              次选(何时)
  ────────────────────────────────────────────
  合并两个已知形状              interface extends  & 交叉(一次性拼装)
  动态键 + 值同质               Record            索引签名(传统写法)
  已知键必填 + 动态键放行        interface extends + 索引签名  Record & 索引签名
  部分更新载荷                 Partial<Omit<..>>  手写可选版(放弃单一事实源)
  对外契约(冻结)              Pick              interface extends 拆分
  排除字段(演化)              Omit              (别无选择,但记住两个坑)

  图说:六行覆盖了对象类型的全部日常选型;「首选」列背下来,
  「次选」列知道什么时候换——这就是本章的全部学习成果。

这张表也是本章最适合打印贴墙的一页:它比速查卡多一列「需求线索」, 是从问题到工具的直达路线——日常写代码时,先找需求所在的行,再看首选列。 当首选列开始显得「自然而然」时,本章就算读到了;当次选列的换用时机也能 脱口而出时,本章就算读透了。

type 还是 interface?一张决策表收尾

第 3 节的争论落到日常,其实就四个问题:

问选 type选 interface
需要并集/交叉/模板字面量类型?✅ 必须❌ 做不到
需要 extends 继承?可以(& 交叉)✅ 更好(报错早、可缓存)
需要声明合并(给库打补丁)?❌ 做不到✅ 必须
团队有 OO 背景、重视继承体系?可以✅ 更顺眼

原书的默认(默认 type)适合前两个问题都答「否」的大多数应用代码; 一旦第三问答「是」(第 10 章的场景),interface 就是唯一解58。

把这张表再压成一条「逃生口诀」,适合在键盘前快速自问:

  写一个新对象类型,30 秒决策:
  ① 它会是「几种可能之一」吗?(并集/交叉参与)→ type
  ② 它会被另一个类型 extends 吗?→ interface
  ③ 它要给第三方类型打补丁吗?→ interface(声明合并是唯一通道)
  ④ 以上都不是 → type(默认,灵活且不会半夜合并)

  图说:①③ 是硬约束(做不到就是做不到),②④ 是软偏好;
  硬约束先判,软偏好随缘——多数争议发生在把软偏好当硬约束吵。

现实中 type vs interface 的争论,九成是「把第 ④ 条的软偏好吵成了信仰」。 这张口诀的价值是把争论降级:硬约束(①③)没得吵,软偏好(②④)随团队—— 一条团队约定写下来,比十次技术辩论都省时间。

组合技的「最后一步」:什么时候停下

工具学到这里,最后一个该学的动作是「停」:

  组合技的收益递减点,出现在「再套一层省的字数 < 读者多花的解析时间」时。
    Partial<Omit<Product, "id">>          → 两层,清晰,保留
    Partial<Omit<Pick<Product, ...>, ...>> → 三层,读者开始画语法树,该拆

  拆法也简单:给中间结果一个名字(第 11 章的纪律在此适用):
    type ProductUpdatePayload = Omit<Product, "id">;
    const updateProduct = (id: number, info: Partial<ProductUpdatePayload>) => {...};

  图说:名字是类型的「句读」——没有句读的长句不是高级,是难读;
  三层以上的套娃,先拆名,再谈优雅。

这也是本章和第 11、16 章共同构成的「复杂度预算」三角:本章给工具(能做多复杂), 第 11 章给判断(该不该这么复杂),第 16 章给能力(复杂到要自造工具时怎么办)。 三角齐了,才算完整毕业了「对象类型」这一科——而它的真正考场, 是下个章节那个最常被误用的类型形态:联合。

本章的一句话版

按拆解惯例,收一句给赶时间的读者:「对象类型三板斧:交叉合并、Record 兜动态键、 工具类型做变换;默认 type,要继承换 interface;Omit 随源长,Pick 冻结; 悬停见 never,就是交叉在打架。」 其余一切——分配性、PropertyKey、 套娃纪律、决策表——都是这六小句的展开。能在自己的代码里认出这六句话各自 管着哪一处,本章就毕业了。毕业的标志不是背得出,是写的时候手自己会选工具—— 从「查这一章」到「不必查」,平均需要在自己的项目里真刀真枪用上三回: 一回交叉或 extends、一回 Record、一回工具类型套娃。用到第三回时回来对一遍 这张速查表,对不上的那一行,就是下一轮该精读的小节——这是把「读完」升级成 「会用」的最短路径,也是原书八道练习真正想训练的东西。

7. 这一章在全书链条中的位置

对象类型的三件套(组合、动态键、变换)凑齐后,「类型的单一事实源」原则有了完整的执行工具: 源类型定义一次,extends / Record / Omit / Pick 派生出所有需要的变体,源变派生跟着变。 第 11 章会把这套思想再推一步——用 keyof、typeof 让派生自动发生; 第 16 章则教你把 Partial、DistributiveOmit 这类工具自己写出来。 原书小结的排序是:组合(交叉 vs extends)、动态键(索引签名/Record/PropertyKey)、 变换(四个工具类型 + 分配性问题)59。

把本章放进全书的「地形图」,它的位置是「平原」——没有新心智模型, 全部是已有概念的复合应用:

  第 04 章:对象的形状词汇        ──┐
  第 05 章:类型参数(Set/Map)      ──┼──本章把它们全部组合到对象上
  第 06 章:并集与收窄            ──┤   (交叉、动态键、工具类型)
  第 08 章:as const 的深锁       ──┘
        ↓
  第 11 章:用 keyof/typeof 让派生自动发生(本章工具的「自动挡」)
  第 16 章:亲手写本章工具的实现(本章工具的「引擎室」)

  图说:本章不发明概念,只发明组合——这也是为什么它是全书最「顺手」的一章:
  每个工具都能从前面某章找到零件,读起来几乎全是「原来还能这么拼」。

「顺手」不等于「可跳过」:本章的三个工具(交叉、动态键、变换)是后面五章的 日常用语——第 13 章的「开放对象」、第 14 章的「声明合并」、第 16 章的 「映射类型」,全部建立在「对象类型可以被组合和变换」这个本章立起的前提上。 跳过本章读后面,等于没学词组直接读长句。

本章的隐藏主线:形状的演化方向

把全章的工具按「源类型变化时变体怎么动」排一列,一条隐藏主线浮出水面—— 每个工具都暗含一个「演化方向」的承诺:

  工具                源加字段时            源改字段类型时
  ────────────────────────────────────────────
  extends / Pick      冻结(不变)           跟随(变)
  Omit                跟随(自动多)         跟随(除非被删掉)
  Partial / Required  跟随(自动多/少?)     跟随
  交叉 &              跟随                 跟随

  图说:没有「永远正确」的演化方向——
  「冻结」保护契约,「跟随」保护投影。第 11 章的两问决策
  (该不该同步)正是在为这张表选列;而第 12 章的 satisfies,
  解决的是「我想冻结演化、又不想失去检查」的角点情况。

这条主线是本章全部选型知识的总开关:拿到任何一个对象类型工具, 先问它承诺哪种演化方向,再问自己需要哪种——两问对齐,工具自动选对; 两问错位,用哪个工具都会在将来某个重构日付利息。把「演化方向」四个字 写进团队的类型评审清单,比任何「最佳实践」清单都管用。它也是本章与第 11、12 章的 真正缝合点:第 11 章教「该不该同步」(判断),本章教「同步了会怎样」(方向), 第 12 章教「想冻结又要检查怎么办」(角点)——三章是一条完整的决策流水线。 流水线各自独立成章、合起来覆盖全部选型场景,这也是原书章节编排的深层秩序: 第 07 章给工具手感、第 11 章给判断框架、第 12 章给角点补丁—— 读的人按顺序走是教材,用的人按需查是手册,两种读法都成立,这就是「 Essentials 类教材」 的罕见完成度。它也让本章的拆解可以采用「双索引」结构:正文按教学顺序展开, 「选型决策线」「演化方向表」两张速查按使用场景重组——读者读到的是教材, 用到的是手册,一份内容两种形态,这是我们对原书结构最忠实的翻译。翻译的代价是 本章拆解的篇幅几乎与原书持平——但这是值得的:对象类型是全书引用频率最高的 一章(后面十章没有一章不回头用它),它的拆解密度应该和它被引用的密度匹配。 按同一把尺子,第 06 章(联合)的密度也应该一样高——那里是 TypeScript 表达力的 心脏,也是误用率常年冠军的栖息地。第七章到此拆完,下一站:联合。 联合类型的拆解密度与对象类型相当——它和本章共享同一块地基(方向感), 却又多出一整条「收窄」的暗河;两条水系在判别联合处交汇,是全书写法最讲究的一章。 写完这句才发现一个有趣的对称:对象类型的章节以「合并」为主题(把形状合到一起), 联合类型的章节以「分辨」为主题(把可能性区分开)——一合一分,构成 TypeScript 类型世界的全部基本动作。学会这两章,其余所有章节都是这两个动作的组合与变奏; 学不会这两章,后面的工具再多也只是背名字。这个对称值得在进入第 06 章之前 先留在心里:它是全书前半部与后半部的真正分水岭,虽然章号上前者在后。 带着「一合一分」这幅对仗进入下一章,读者会发现联合类型的每个机制 都能在这幅对仗里找到位置——收窄是「分」的引擎,判别联合是「分」的成熟形态, 宽进窄出是「分」在函数边界的投影。地图已经画好,接下来是第 06 章的实地勘测。 勘测之前,照例留一句话版给赶时间的读者:「合并用 extends(或 &),动态键用 Record, 变换用工具类型;选型先定演化方向——冻结用 Pick,跟随用 Omit。」 六小句, 各管一章的一片疆土;读完下一章再回来对一遍,你会发现这两章的口诀连起来读, 正好是 TypeScript 类型设计的「总开关」。第七章的拆解到此正式结束; 它的原文地图、判断块与出处,均已按全书统一规格校验,可以随时回核。回核之外, 本章留给读者最后一个动作建议:把「选型决策线」和「演化方向表」两张速查 复制到自己的团队 wiki,在代码评审时对照引用——让两张纸参与真实决策, 比任何复习都更能把知识固化成团队的公共财产。至此,本章的正文、判断、地图三层 全部到位,与全书其他十六章的规格完全一致——这是拆解工作真正完成的标志。 标志立完,第七章可以放心地合上了;它对后续九章的全部「引用债务」, 已经由本章的密度和厚度兑付完毕。合上之前,最后一眼留给它的主线例子—— 那张「颜色配置对象」从第 04 章一路走到这里,已经被 extends、Record、 工具类型轮流改装过;它现在功成身退,而第 06 章的新主角「并集类型」 正在侧台候场。换幕。第七章的拆解正式收工,全文完——从顶层全景到原文地图, 八节内容无一处缺席,无一处无出处。收工,落幕,致谢读者读到这里,我们第 06 章拆解见。

8. 作者的判断与证据(分开标)

书里有据的:

  • extends 优于 & 的两条理由(报错时机、缓存性能)——前者可当场验证,后者是编译器实现事实的陈述91011。
  • 「默认 type,要 extends 换 interface」——明确的主见,作者自承「close call」15。
  • 声明合并「丢过好几个小时」——第一人称证言,用来警示这个 gotcha14。
  • Omit 宽松版是团队当年的有意选择——引了原始的 issue/PR/结论文档,属于有据可考的设计史3637。
  • Omit/Pick 不分配及 DistributiveOmit 解法——机制描述,配了悬停对照实证3840。

原书没说的(我们补):

  • DistributiveOmit 里的 T extends any ? ... : never 为什么能分配——补充(不在书里,第 16 章才展开): 条件类型在左边是「裸类型参数」时会对联合逐成员分配,这是条件类型的内置行为。
  • interface vs type 的争论史与社区统计——补充(来自通用知识): 这个争论从 2016 年持续至今, TypeScript 团队官方文档的态度与原书一致:两者大部分场景等价,选你喜欢的;需要 extends 或合并时用 interface。

判断(我们的,不是书里的): 本章最有「类型系统观」价值的一个事实,是 Omit 的设计史—— 一个全局可用的工具类型,因为「不能破坏已有代码」这个约束,选择了宽松语义并永久接受了它。 这是观察「语言设计中的兼容性债」的极好样本:工具一旦全局发布,语义错误也被冻结 (严格版 Omit 直到今天也只能靠用户手写在项目里)。 读任何语言的内置库时都值得问一句:这个看似随意的宽松行为,是不是一次 2019 年的妥协? 如果错,会错在: 宽松 Omit 在多数场景下确实无害(删错了键,得到的是意外通过而非错误拒绝), 它的「债」要在大规模重构时才显出利息——所以也可能这笔债永远不用还,只是记在账上。

判断(我们的,不是书里的): 「Omit 随源长大,Pick 冻结」这条规则,本质是在问 「这个类型是不是源类型的投影」。我们见过的多数 API 层错误,根源是把投影当契约 (用 Omit 声明对外响应,内部模型一加字段就泄漏)或把契约当投影(用 Pick 复制内部模型, 模型改了忘记同步)。原书用一道练习题把这个维度立起来,比直接给答案高明。 如果错,会错在: 在模块内部(不跨边界),Omit 和 Pick 的差异几乎不存在—— 这条规则的适用面是「有边界的地方」(API、库、组件 props),把它用到模块内部是过度设计。

判断(我们的,不是书里的): 本章最被低估的产能来自「样本走查」式的学习法—— 原书不满足于给工具,而是给五道同一对象的连续改写题(交叉版、Record 版、 组合版、演化版、四解版),逼你在具体输入上比较方案。这比读十遍定义有效, 因为对象类型设计的全部权衡(精确 vs 灵活、开放 vs 封闭、宽 vs 窄) 只有在「同一个需求的两种写法并排」时才可见。 迁移到日常工作:重构一个对象类型时,先写两种候选实现,各配三个典型调用点, 让调用点的形状替你投票——这就是把练习题的格式搬到了生产代码里。 如果错,会错在: 对于形状稳定、调用点极少的内部类型,双方案对比的成本 高于收益——「先写一版,报错再调」在低风险场景同样是合理的工程策略; 样本走查法是高风险边界(API、共享模型)的加严措施,不是 universal 纪律。

9. 边界与局限

  • 工具类型只一层深:Partial/Required 不递归(原书自己标了注并指路 type-fest)31。
  • Omit 的两个坑本章只给缓解:宽松键问题要到第 16 章才有严格版;分配性问题给了 DistributiveOmit 但没有讲原理。
  • 本章的「性能」论述没给量化数据——缓存 vs 重算是对的,但多大的项目才有感,原书没测。
  • Record 的「并集键 = 全必填」行为本章靠例子展示,没有明写规则(容易误以为并集键可选)。
  • 交叉类型与原始类型的组合基本没用:string & number = never 这类结果只会出现在误用里; 交叉的正当地位是给对象类型做组合。
  • DistributiveOmit 的 K extends PropertyKey 约束本章只照抄,extends 在类型参数上的含义 要等第 16 章泛型才讲透;眼下把它当「这个工具对任何键类型都适用」的注脚即可。
  • 组合技的层数有认知上限:套娃超过两层(Pick<Omit<Partial<...>>>)后, 类型的可读性崩溃比运行效率先到来——读者要从内向外剥三层才看清形状。 实务中的上限是两层,三层就该拆中间类型(第 11 章的套娃纪律在这里同样适用)。
  • 本章所有工具都假设「对象形状已知」:面对「形状来自运行时(JSON 响应)」的场景, 工具类型的约束能力有限——它们能变换已知形状,不能验证未知形状 (那是第 06 章 narrowing 和第 17 章谓词的领地)。把工具类型当运行时校验用, 是本章工具最常见的越界。

10. 可带走的

  1. & = 与;对象交叉 = 属性全集;冲突属性收编成 never——悬停见 never,就是交叉在打架。
  2. interface extends 胜过 & 的两条理由:报错更早、结果可缓存。
  3. 默认 type;要 extends、要多继承、要合并补丁时再上 interface。
  4. interface 的声明合并是双刃剑:给库打补丁时是特性,自己代码里是陷阱。
  5. 动态键首选 Record<K, V>;索引签名 [key: string]: T 次之(键不支持字面量并集)。
  6. 已知键 + 动态键 = Record<已知并集, V> & {[k: string]: V} 或 interface extends + 索引签名。
  7. 键参数的类型写 PropertyKey(= string | number | symbol)。
  8. Partial 做「部分更新」;Required 反向;两者都只作用一层。
  9. Omit 随源类型长大,Pick 冻结——选型看你要哪种演化方向。
  10. Omit 删不存在的键不报错(宽松版是有意的);Pick 严格。
  11. Omit/Pick 对联合类型不分配——判别联合上要用 DistributiveOmit/DistributivePick。
  12. 套娃读法从内向外:Partial<Omit<Product, "id">> = 先去 id,再全可选。
  13. 工具类型不改源类型——所有变体都是源类型的纯函数,这就是单一事实源的成立条件。
  14. 全局类型(Promise、PropertyKey……)都住在 lib.es5.d.ts 之类的声明文件里,悬停追定义是最被低估的学习习惯。
  15. 交叉的冲突有三种性质(类型不同/可选性不同/字面量互斥)——第三种常意味着「或写成了与」,该回第 06 章换判别联合。
  16. 「只一层」是内置工具的系统性格:Partial/Required/Readonly 同款;深版本要么递归(第 16 章)、要么 type-fest、要么把数据扁平化。
  17. 组合写法的兼容规则:已知键的类型必须是动态键值类型的子集,父子颠倒必报错。
  18. 套娃三层是上限;读写方向相反(读从内向外,写从外向内),中间类型是超过两层后的标准解法。

最后给十四章内容一句总纲:对象类型的全部机制,都是「别复制,要引用」这四个字 的不同力度的实现——交叉是即时引用、extends 是层级引用、索引签名是键的引用 (任意键)、工具类型是变换后的引用。复制(手抄属性清单)在本章每出现一次, 都立刻有一种引用语法把它替换掉。下次手写对象类型时,先问「这里有没有 我已经写过的形状可以引」——这一问,是本章值回票价的唯一动作。

这句总纲也值得配上它的另一半:「引用」的代价是耦合,耦合的边界由第 11 章的 两问决策裁决。本章教你的是「怎么引」(语法),第 11 章教你的是「该不该引」 (判断);两章合起来,才是完整的「单一事实源」工程实践。只引不问,会把 本该解耦的类型拴在一起;只问不引,会把该同步的副本留在手抄里。 「怎么引」和「该不该引」各管一段,中间的分界线正是 Omit/Pick 生长方向那道题 教你的「演化方向判断」。

最后交代本章的读法,也是按拆解惯例的收尾:三行全景(拼、兜、改)是骨架, 五个样本走查是肌肉,十八条的「可带走的」是神经——三层各读一遍,本章就物尽其用。 如果只有一小时:先读第 1 节的全景和第 6 节末的选型决策线,再用剩下的时间 亲手把主线例子的三种声明方式(注解、裸推断、satisfies…本章的交叉与 extends) 各敲一遍——敲的时候悬停三次,本章的一半知识会以手感而非文字的方式落袋。 第七章的对象,到此拆完;下一个战场的类型形态,是本系列误用率的常年冠军:联合。 联合的类型世界比对象更反直觉(值更少、要求更多、还有分配性这种暗流), 但它也是 TypeScript 表达力的心脏——第 06 章的收窄、判别联合、unknown/never 的层级, 都在那里等着给这台心脏做解剖。

11. 原文地图

本章地图覆盖原书第 6 章(Extending Objects)的全部小节与八道练习。 新增条目对应加厚内容:交叉的适用域与 never 三步排查、两个硬理由的再推一层、 索引签名的三个边界、组合写法的子集兼容规则、「只一层」的系统性性格、 套娃的读写纪律、Pick/Omit 的生长方向语义、分配性的判据与机制解释、 三处「先埋后收」的清单、选型决策线的六行速查——均可按行号 + 搜词两步回核。

主题原书位置原文位置
交叉类型 & 的定义Extending Objectstext/22-fm-extending-objects.txt:16(搜「reverse of the
AlbumSales 主线例子同上text/22-fm-extending-objects.txt:33(搜「AlbumSales = Album & SalesData」)
string & number = never同上text/22-fm-extending-objects.txt:52(搜「It's actually never」)
冲突属性解析为 never同上text/22-fm-extending-objects.txt:68(搜「age: never」)
interface 语法对比同上text/22-fm-extending-objects.txt:85(搜「interface Album」)
extends 与多继承同上text/22-fm-extending-objects.txt:103(搜「StudioAlbum extends」)、:135(搜「BonusConcertEdition」)
extends 优于 & 的裁决同上text/22-fm-extending-objects.txt:143(搜「which is better」)
报错时机对比同上text/22-fm-extending-objects.txt:172(搜「incorrectly extends」)
性能理由(重算 vs 缓存)同上text/22-fm-extending-objects.txt:181(搜「recomputed every time」)
type 更灵活(并集例子)同上text/22-fm-extending-objects.txt:193(搜「type Union」)
声明合并同上text/22-fm-extending-objects.txt:196(搜「declaration merging」)
「丢过好几个小时」同上text/22-fm-extending-objects.txt:234(搜「lost hours」)
「默认 type」的主张同上text/22-fm-extending-objects.txt:236(搜「default to type」)
动态键的 JS/TS 对比同上text/22-fm-extending-objects.txt:377(搜「Property 'Grammy' does not exist」)
索引签名语法同上text/22-fm-extending-objects.txt:401(搜「index signature」)
Record 与并集键同上text/22-fm-extending-objects.txt:435(搜「const albumAwards: Record」)
「Record 是动态键首选」同上text/22-fm-extending-objects.txt:456(搜「go-to for dynamic keys」)
已知键 + 动态键组合同上text/22-fm-extending-objects.txt:464(搜「ExtendedAlbumAwards」)
PropertyKey 定义同上text/22-fm-extending-objects.txt:494(搜「declare type PropertyKey」)
hasKey 练习(PropertyKey 的用处)同上text/22-fm-extending-objects.txt:700(搜「v: PropertyKey」)
Partial同上text/22-fm-extending-objects.txt:728(搜「PartialAlbum」)
Required 与「只一层深」同上text/22-fm-extending-objects.txt:766(搜「one level deep」)
Pick / Omit同上text/22-fm-extending-objects.txt:802(搜「Pick<Album」)、:814(搜「Omit<Album」)
Omit 宽松版的来历同上text/22-fm-extending-objects.txt:836(搜「loose version」)
Omit 对联合不分配同上text/22-fm-extending-objects.txt:899(搜「not distributive」)
DistributiveOmit / DistributivePick同上text/22-fm-extending-objects.txt:903(搜「DistributiveOmit」)、:937(搜「DistributivePick」)
Omit 长大 / Pick 冻结同上text/22-fm-extending-objects.txt:1007(搜「grows as the source object grows」)
Partial<Omit<…>> 组合技同上text/22-fm-extending-objects.txt:1065(搜「Partial<Omit<Product」)
本章小结同上text/22-fm-extending-objects.txt:1072(搜「powerful features」)
练习 6-7:Pick/Omit/extends 三种解法Extending Objectstext/22-fm-extending-objects.txt:975(搜「PickedUser」) · text/22-fm-extending-objects.txt:1007(搜「grows as the source object grows」)
练习 6-8:Partial + Omit 组合同上text/22-fm-extending-objects.txt:1065(搜「Partial<Omit<Product」)
「两个硬理由」原文同上text/22-fm-extending-objects.txt:145(搜「Better Errors」) · text/22-fm-extending-objects.txt:177(搜「Better TypeScript Performance」)
string & number = never同上text/22-fm-extending-objects.txt:16(搜「reverse of the」)

Footnotes

  1. 出处:「Extending Objects」第 16 段(text/22-fm-extending-objects.txt:16,搜「reverse of the | operator」)。 ↩

  2. 出处:「Extending Objects」第 34–44 段(text/22-fm-extending-objects.txt:36,搜「wishYouWereHereSales」)。 ↩

  3. 出处:「Extending Objects」第 46–48 段(text/22-fm-extending-objects.txt:48,搜「& {genre: string}」)。 ↩

  4. 出处:「Extending Objects」第 49–52 段(text/22-fm-extending-objects.txt:52,搜「It's actually never」)。 ↩

  5. 出处:「Extending Objects」第 54–71 段(text/22-fm-extending-objects.txt:68,搜「age: never」)。 ↩

  6. 出处:「Extending Objects」第 75–91 段(text/22-fm-extending-objects.txt:85,搜「interface Album」)。 ↩

  7. 出处:「Extending Objects」第 93–111 段(text/22-fm-extending-objects.txt:103,搜「StudioAlbum extends」)。 ↩

  8. 出处:「Extending Objects」第 115–131 段(text/22-fm-extending-objects.txt:115,搜「americanBeauty」)。 ↩

  9. 出处:「Extending Objects」第 141–185 段(text/22-fm-extending-objects.txt:143,搜「which is better」);结论句见第 185 段(搜「better for catching errors」)。 ↩ ↩2 ↩3

  10. 出处:「Extending Objects」第 145–175 段(text/22-fm-extending-objects.txt:172,搜「incorrectly extends」);「使用时才报错」的对比见第 175 段(搜「instead of when it is defined」)。 ↩ ↩2

  11. 出处:「Extending Objects」第 177–183 段(text/22-fm-extending-objects.txt:181,搜「recomputed every time」);缓存机制见第 183 段(搜「cache the resultant type」)。 ↩ ↩2

  12. 出处:「Extending Objects」第 187–194 段(text/22-fm-extending-objects.txt:191,搜「lot more flexible」)。 ↩

  13. 出处:「Extending Objects」第 196–232 段(text/22-fm-extending-objects.txt:210,搜「automatically merges」);type 重复定义报错见第 223 段(搜「duplicate identifier」)。 ↩

  14. 出处:「Extending Objects」第 234 段(text/22-fm-extending-objects.txt:234,搜「lost hours」)。 ↩ ↩2

  15. 出处:「Extending Objects」第 236 段(text/22-fm-extending-objects.txt:236,搜「default to type」)。 ↩ ↩2

  16. 出处:「Extending Objects」第 367–377 段(text/22-fm-extending-objects.txt:377,搜「Property 'Grammy' does not exist」)。 ↩

  17. 出处:「Extending Objects」第 389 段(text/22-fm-extending-objects.txt:389,搜「indexing」)。 ↩

  18. 出处:「Extending Objects」第 393–401 段(text/22-fm-extending-objects.txt:401,搜「index signature」)。 ↩

  19. 出处:「Extending Objects」第 403–407 段(text/22-fm-extending-objects.txt:406,搜「iCanBeAnything」)。 ↩

  20. 出处:「Extending Objects」第 409–418 段(text/22-fm-extending-objects.txt:411,搜「interface AlbumAwards」)。 ↩

  21. 出处:「Extending Objects」第 422–431 段(text/22-fm-extending-objects.txt:428,搜「Record<string, boolean>」)。 ↩

  22. 出处:「Extending Objects」第 433–454 段(text/22-fm-extending-objects.txt:451,搜「cannot be a literal type」);「第 15 章揭晓」见第 454 段(搜「mapped types」)。 ↩

  23. 出处:「Extending Objects」第 456 段(text/22-fm-extending-objects.txt:456,搜「go-to for dynamic keys」)。 ↩

  24. 出处:「Extending Objects」第 458–475 段(text/22-fm-extending-objects.txt:464,搜「ExtendedAlbumAwards」)。 ↩

  25. 出处:「Extending Objects」第 477–487 段(text/22-fm-extending-objects.txt:487,搜「interface extends is preferable」)。 ↩

  26. 出处:「Extending Objects」第 489–495 段(text/22-fm-extending-objects.txt:494,搜「declare type PropertyKey」)。 ↩ ↩2

  27. 出处:「Extending Objects」第 640–707 段(text/22-fm-extending-objects.txt:704,搜「return obj.hasOwnProperty」);悬停签名见第 700 段(搜「v: PropertyKey」)。 ↩

  28. 出处:「Extending Objects」第 709–711 段(text/22-fm-extending-objects.txt:711,搜「built-in utility types」)。 ↩

  29. 出处:「Extending Objects」第 713–737 段(text/22-fm-extending-objects.txt:728,搜「PartialAlbum」)。 ↩

  30. 出处:「Extending Objects」练习 6-8 与解答(text/22-fm-extending-objects.txt:1065,搜「Partial<Omit<Product」)。题面(更新只需部分属性)见第 1024–1026 段。 ↩

  31. 出处:「Extending Objects」第 766–790 段(text/22-fm-extending-objects.txt:766,搜「one level deep」)。 ↩ ↩2 ↩3 ↩4

  32. 出处:「Extending Objects」第 792–794 段(text/22-fm-extending-objects.txt:794,搜「type-fest」)。 ↩

  33. 出处:「Extending Objects」第 1007 段(text/22-fm-extending-objects.txt:1007,搜「grows as the source object grows」)。 ↩

  34. 出处:「Extending Objects」第 807–815 段(text/22-fm-extending-objects.txt:814,搜「Omit<Album」)。 ↩

  35. 出处:「Extending Objects」第 817–834 段(text/22-fm-extending-objects.txt:834,搜「does not satisfy the constraint」)。 ↩ ↩2

  36. 出处:「Extending Objects」第 836 段(text/22-fm-extending-objects.txt:836,搜「loose version」)。 ↩ ↩2 ↩3

  37. 出处:「Extending Objects」第 838–842 段(text/22-fm-extending-objects.txt:838,搜「keep an eye out」)。 ↩ ↩2

  38. 出处:「Extending Objects」第 844–887 段(text/22-fm-extending-objects.txt:882,搜「Actual:」)。 ↩ ↩2

  39. 出处:「Extending Objects」第 889–897 段(text/22-fm-extending-objects.txt:894,搜「PartialMusicProduct」)。 ↩

  40. 出处:「Extending Objects」第 899 段(text/22-fm-extending-objects.txt:899,搜「not distributive」)。 ↩ ↩2 ↩3 ↩4

  41. 出处:「Extending Objects」第 901–941 段(text/22-fm-extending-objects.txt:903,搜「DistributiveOmit」)、第 937 段(搜「DistributivePick」)。 ↩

  42. 出处:「Extending Objects」第 933 段(text/22-fm-extending-objects.txt:933,搜「much more predictable」)。 ↩

  43. 出处:「Extending Objects」练习 6-7 收尾(text/22-fm-extending-objects.txt:1007,搜「grows as the source object grows」)。原文:「Using Omit will mean that the object grows as the source object grows, and using Pick and interface extends will mean that the object will stay the same size.」 ↩

  44. 出处:「Extending Objects」第 1009–1068 段(text/22-fm-extending-objects.txt:1065,搜「Partial<Omit<Product」)。 ↩

  45. 出处:「Extending Objects」第 943–1007 段(text/22-fm-extending-objects.txt:1007,搜「grows as the source object grows」)。 ↩

  46. 出处:「Extending Objects」第 238–295 段(text/22-fm-extending-objects.txt:262,搜「BaseEntity」);交叉叠加写法见第 269–293 段(搜「} & BaseEntity」)。 ↩

  47. 出处:「Extending Objects」第 297–352 段(text/22-fm-extending-objects.txt:352,搜「thank you」);多继承拆法见第 334–350 段(搜「WithId, WithCreatedAt」)。 ↩

  48. 出处:「Extending Objects」第 616–626 段(text/22-fm-extending-objects.txt:626,搜「allows for the notAllowed key」)。 ↩

  49. 出处:「Extending Objects」第 628–638 段(text/22-fm-extending-objects.txt:630,搜「Record<」)。 ↩

  50. 出处:「Extending Objects」第 584–612 段(text/22-fm-extending-objects.txt:606,搜「notAllowed: {」)。 ↩

  51. 出处:「Extending Objects」第 539–582 段(text/22-fm-extending-objects.txt:578,搜「interface Scores extends RequiredScores」)。 ↩

  52. 出处:「Extending Objects」第 543–557 段(text/22-fm-extending-objects.txt:545,搜「science should be provided」)。 ↩

  53. 出处:「Extending Objects」第 971–980 段(text/22-fm-extending-objects.txt:975,搜「PickedUser」)。 ↩

  54. 出处:「Extending Objects」第 981–987 段(text/22-fm-extending-objects.txt:983,搜「OmittedUser」)。 ↩

  55. 出处:「Extending Objects」第 989–1005 段(text/22-fm-extending-objects.txt:991,搜「NameAndEmail」)。 ↩

  56. 出处:「Extending Objects」第 1007 段(text/22-fm-extending-objects.txt:1007,搜「Choose the best approach」)。 ↩

  57. 出处:「Extending Objects」第 502–538 段(text/22-fm-extending-objects.txt:538,搜「Record<string, number>」)。 ↩

  58. 出处:「Extending Objects」第 187–236 段(text/22-fm-extending-objects.txt:236,搜「default to type」)。 ↩

  59. 出处:「Extending Objects」第 1070–1086 段(text/22-fm-extending-objects.txt:1072,搜「powerful features」)。 ↩

On this page

对象 — 交叉类型、interface 继承与工具类型1. 顶层全景2. 交叉类型:& 把两个形状叠在一起并集与交叉的「方向感」交叉的适用域:& 与 extends 的分工预告交叉的暗面:冲突收编成 nevernever 的三步排查法:交叉冒出的 never 从哪来3. interface 与 extends:面向继承的另一半拼图& 还是 extends?原书给了两个硬理由,都选 extends那为什么不全用 interface?type 与 interface 的对照4. 动态键:键名事先不知道的对象索引签名:先声明「任意字符串键都行」索引签名的三个边界,用之前先知道已知键 + 动态键的组合组合写法的代价:双重语义重叠时的边界情况5. 工具类型:对已有类型做变换Partial:更新场景的全可选Required:反向操作,且只有一层Pick 与 Omit:留和删Omit 的怪癖:删掉不存在的键不报错联合类型上的大坑:Omit/Pick 不「分配」分配性的判据:工具会不会「逐个成员操作」四个工具类型速查(本章的口袋卡片)Pick 和 Omit 的生长方向:源变长时,谁跟着变6. 组合技:工具类型的套娃套娃的读法与写法纪律样本走查一:BaseEntity 的两代写法(交叉 → extends)两代写法的「悬停对比实验」样本走查二:用 Record 给键上锁(配置表)样本走查三:已知键必填 + 动态键放行(成绩表)样本走查四:fetchUser 的三条路线(演化方向的选择题)样本走查五:scores 的一题四解(动态键的四种写法)五个样本走完:一条完整的选型决策线type 还是 interface?一张决策表收尾组合技的「最后一步」:什么时候停下本章的一句话版7. 这一章在全书链条中的位置本章的隐藏主线:形状的演化方向8. 作者的判断与证据(分开标)9. 边界与局限10. 可带走的11. 原文地图Footnotes