设计类型 — 泛型类型、模板字面量类型与映射类型¶
这一章讲什么: 前三部分(基础类型、并集、推导)解决的是「把一个类型写对」; 这一章解决的是「让一组类型一起写对」——当你发现两个类型长得几乎一样、 只有一处不同时,该怎么把那一处抽出来,造一个可复用的「类型模具」。 原书给的答案是三件工具:泛型类型(把类型参数化)、模板字面量类型(让字符串格式可被检查)、 映射类型(批量改写形状的键和值),外加一件备而不用的重型武器:条件类型。
1. 顶层全景:三个工具,一条「消灭重复」的线¶
两个类型长得几乎一样,只有一处不同
│
├─ 不同的那一处是「另一个类型」 → 泛型类型:<T> 把那一处变成参数
├─ 不同的那一处是「字符串的格式」 → 模板字面量类型:`${string}.png`
└─ 不同的那一处是「键和值的批量变换」 → 映射类型:[K in keyof T]
└─ 改键名还要留住原键 → as 重映射
图说:三样工具都服务于同一件事——类型层面的 DRY(不重复自己)。
第 11 章的 keyof/typeof 是「从一个类型取东西」,本章是「造一个能反复用的类型机器」。
一个全书层面的预告:第 16 章造「类型的机器」,第 17 章造「函数里用类型的机器」—— 两章合起来就是「设计类型」这一篇的完整内容:先会造类型层面的抽象, 再会把这些抽象装进函数、交给团队复用。
这一章的全章主线例子是流媒体的「资源状态」。先把两个原始类型摆出来, 后面每个机制都在它们身上走一步:
type StreamingPlaylist =
| {
status: "available";
content: { id: number; name: string; tracks: string[] };
}
| {
status: "unavailable";
reason: string;
};
type StreamingAlbum =
| {
status: "available";
content: { id: number; title: string; artist: string; tracks: string[] };
}
| {
status: "unavailable";
reason: string;
};
这两个类型描述的是同一件事:一个流媒体资源,要么「可用、带上具体内容」,
要么「不可用、带上原因」。两段结构只有一处差别:content 对象里的属性
(playlist 是 name,album 是 title + artist)1。
问题很具体: 如果哪天「可用」分支里要加一个所有资源共有的字段(比如 metadata),
你得在两个类型里各改一遍,漏一个就是 bug 的温床。能不能把「同样的骨架、不同的内容」
抽象成一个模具?——这就是泛型类型要解决的2。
2. 主线例子第一幕:ResourceStatus,一个类型模具的诞生¶
第 ① 步:先声明一个「收类型参数」的类型¶
原书先把模具的架子立起来:
尖括号是之前给 Set<string>、Pick<T, K> 传类型实参时用过的记号;
现在它出现在类型名字后面,意思整个反过来——
尖括号里的是这个类型自己声明的「类型参数」:ResourceStatus 声明自己收一个叫
TContent 的参数,将来谁用它,谁就得提供一个类型填进来。
参数名可以随便起;行内惯例是用 T、K、V 这样的单字母,起成 TContent 这样「T + 名词」
的自解释名也完全合法3。
先留意一件事:模具的「身体」现在写的是 unknown(未知,类型世界的「什么都行」)。
这意味着不管传什么进来,结果都是 unknown。怎么用?传一个试试:
type Example = ResourceStatus<{ id: string; name: string; tracks: string[] }>;
// 悬停 Example → unknown。悬停 TContent,编译器提示:
// Type 'TContent' is declared but its value is never read.
// (TContent 已声明,但它的值从未被读取)
报错信息就是线索:TContent 没有被用到——模具收了参数,却把它晾在一边,
当然什么形状都还是 unknown4。
第 ② 步:把参数填进骨架¶
type ResourceStatus<TContent> =
| {
status: "available";
content: TContent;
}
| {
status: "unavailable";
reason: string;
};
唯一的变化:content 的类型从写死的对象,换成了类型参数 TContent。
现在两个原类型可以重写成「调用模具」:
type StreamingPlaylist = ResourceStatus<{
id: number;
name: string;
tracks: string[];
}>;
type StreamingAlbum = ResourceStatus<{
id: number;
title: string;
artist: string;
tracks: string[];
}>;
悬停 StreamingPlaylist,看到的结果和原来手写的形状一模一样—— available 分支带各自的 content,unavailable 分支带 reason5。 骨架一处定义,两处生效;以后要加共有字段,只改 ResourceStatus 一处。
原书给这类东西的名字值得记住:ResourceStatus 现在是一个泛型类型, 它是一种「类型函数」——收类型实参、返回新类型,和运行时的函数收值、返回值, 是同构的两件事。它在类型世界里的用处,也就是函数在值世界里的用处: 把重复的模式抓成一个可复用的抽象6。
第 ③ 步:两个参数¶
骨架里还有第二个「将来可能各处不同」的位置——元数据(metadata,描述资源本身的数据, 比如创建者、封面图、更新时间)。 再声明一个类型参数就行:
type ResourceStatus<TContent, TMetadata> =
| {
status: "available";
content: TContent;
metadata: TMetadata;
}
| {
status: "unavailable";
reason: string;
};
playlist 和 album 各自提供自己的 metadata 形状——playlist 的元数据讲「创作者」:
type StreamingPlaylist = ResourceStatus<
{ id: number; name: string; tracks: string[] },
{ creator: string; artwork: string; dateUpdated: Date }
>;
album 的元数据讲「唱片工业」:
type StreamingAlbum = ResourceStatus<
{ id: number; title: string; artist: string; tracks: string[] },
{ recordLabel: string; upc: string; yearOfRelease: number }
>;
结构仍然共用7——两个 TMetadata 实参的形状差异很大,但它们在骨架里占据的是同一个位置、
受同一条 extends object 约束管辖。这正是「类型函数」的精髓:
实参可以千差万别,模具对它们一视同仁。
类型参数想声明几个都可以——但原书立刻补了一句和函数一模一样的话:
参数越多,类型越容易复杂8。这句话在后面每一个机制上都会回响。
第 ④ 步:不传实参,会怎样¶
泛型类型和函数在这一点上不同:用泛型类型而不给类型实参,直接报错:
type Example = ResourceStatus;
// ❌ Generic type 'ResourceStatus' requires 2 type argument(s).
// (泛型类型 ResourceStatus 需要 2 个类型实参)
默认情况下,所有类型实参都必须提供——原书强调「就像运行时的函数一样」9。 (记住这个对比:下一章会看到,泛型函数恰恰在这一点上宽松得多—— 不传类型实参时它会从运行时的实参里推断。一个必传、一个可省, 是「泛型类型」和「泛型函数」最重要的行为差异,也是初学者最容易混的一点。)
第 ⑤ 步:给参数默认值,让它「可选」¶
有些参数你不想每次都传。和函数一样,用等号给默认值:
TMetadata = {} 的意思是:不传第二个实参时,metadata 按空对象 {} 算。
于是不传 metadata 的 playlist 也合法了,悬停可见 metadata 的类型就是 {}10。
默认值让模具更灵活、更好用——这一条与函数默认参数的直觉完全一致。
第 ⑥ 步:给参数加约束,挡住离谱的实参¶
默认值管「不传」;约束管「传错了」。还是 extends:
type HasId = { id: number };
type ResourceStatus<
TContent extends HasId,
TMetadata extends object = {},
> = /* 同上 */;
两个约束各管一件事:
TContent extends HasId:content 必须带id属性(两个资源确实都有 id);TMetadata extends object:metadata 可以不传,但传了必须是对象11。
违反约束时,报错信息直接告诉你差在哪:
type StreamingPlaylist = ResourceStatus<
{ name: string; tracks: string[] }, // ❌ 红线画在这个对象上
{ creator: string }
>;
// Type '{name: string; tracks: string[];}' does not satisfy the constraint 'HasId'.
// Property 'id' is missing in type '{name: string; tracks: string[];}'
// but required in type 'HasId'.
// (该类型不满足约束 HasId:缺少属性 id,而 HasId 要求有 id)
把 id 补上,报错消失12。
一个容易误会的细节:这些约束是「开放的」,不是「封闭的」。 只要带了 id,你往 content 里塞多少额外属性都行——超出的属性会被收进结果类型, 不会触发多余属性检查。原书原话:these constraints are open, not closed13。 (这和第 13 章「对象是开放的」是同一个世界观。)
模具收编两个类型,前后对照¶
把第一幕收到的成果摆在一起看,抽象的价值就一目了然:
收编前 收编后
────────────────────────────── ──────────────────────────────────
StreamingPlaylist: 两段并集, StreamingPlaylist = ResourceStatus<
content 手写 name 等 3 个属性 { id: number; name: string; ... }>
StreamingAlbum: 两段并集, StreamingAlbum = ResourceStatus<
content 手写 title 等 4 个属性 { id: number; title: string; ... }>
共有骨架(available/unavailable ResourceStatus 一处定义,
两个分支)出现 2 次 两处只填差异
图说:重复从「两处各一遍」变成「一处定义 + 两处填空」。
以后骨架要改(比如 available 分支加字段),只动 ResourceStatus。
注意收编后两个类型仍然各自完整:悬停任何一处,展开后的形状和收编前逐字段相同5。 抽象没有把信息藏起来——展开视图永远查得到——它只把「维护点」收敛到了一处。 这是判断一个类型抽象好坏的第一条标准:展开视图无损,维护点收敛。
第二条标准是「差异确实只剩参数」:如果哪天 playlist 和 album 的分支结构也开始分叉 (一个多一个分支),ResourceStatus 就装不下它们了,硬套反而要加一堆条件分支。 原书挑的这个例子(同骨架、异内容)是泛型类型的最佳适用区; 骨架本身开始分叉时,就该回到第 06 章的判别联合老老实实手写。
第 ⑦ 步:一个悬停小技巧——在模具定义处悬停参数名¶
遇到泛型类型行为不对时,原书示范的排查动作是在定义处悬停类型参数本身: 「参数已声明但从未读取」这类提示(第 ① 步见过)只在这种悬停下才出现。 它和调用处悬停互补——一个看「模具的管线接没接」,一个看「这次调用展开成什么」。 第 ① 到 ⑥ 步其实走了一个完整的调试环:声明 → 悬停定义 → 使用 → 悬停结果 → 修正。
3. extends 的三四个意思¶
到这里,extends 已经在三种地方出现过。原书专门停下来列了一遍,
因为它的「一个关键字、多个意思」正是 TypeScript 被吐槽最多的习惯之一14:
| extends 出现在哪 | 意思 |
|---|---|
泛型类型的类型参数上:TContent extends HasId |
约束:传进来的类型必须是 HasId 的子集 |
类上:class Album extends Item |
继承:专辑是一种条目 |
interface 上:interface Album extends Item |
同样是继承/组合接口成员 |
条件类型里:T extends any[] ? T : T[] |
测试:T 能不能赋给 any[] |
同一个词,四个上下文,四种语义——读到 extends 时先问「这是哪一个」,
能避开大量误读。有个不算技巧的技巧:看后面跟的是什么。
后面是类型名 + 等号/大括号,大概率是泛型约束;后面是类体,是继承;
后面是 ?,是条件类型测试;前面是 interface 或 class 关键字,是继承。
词性由位置决定——这和下一章「泛型函数类型别名 vs 泛型类型」的判定法
(看尖括号贴着谁)是同一种思路:TypeScript 里一个记号的含义,往往不在记号本身,而在它的位置14。
(它和 as 的多义性遥相呼应:第 12 章的断言、本章第 6 节的重映射,
也是同一个词的两张脸。)
4. 第二件工具:模板字面量类型——让字符串格式也能被查¶
解决什么问题¶
应用里有大量「格式正确的字符串」:文件路径必须以 .png 结尾、路由必须以 / 开头、 十六进制颜色必须是 # 加六位。这些约束平时只能靠运行时正则或者干脆靠自觉。 模板字面量类型把这类检查挪进编译期。
名字的来历:JavaScript 的模板字符串(反引号字符串)可以把值插进字符串; 模板字面量类型把「类型」插进字符串类型。看一个最小的例子:
type PngFile = `${string}.png`;
let myImage: PngFile = "my-image.png"; // ✅
let myIcon: PngFile = "my-image.jpg"; // ❌ Type '"my-image.jpg"' is not assignable
// to type 'PngFile'.
${string} 是「任意字符串」这个类型的插值位置,.png 是写死的尾巴。
类型层面,一个「以 .png 结尾的字符串」就是这么声明的15。
和并集组合:生成所有拼写¶
单个 ${string} 只是开胃菜。把并集塞进插值位置,模板字面量类型会生成所有组合的并集:
type ColorShade = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
type Color = "red" | "blue" | "green";
type ColorPalette = `${Color}-${ColorShade}`;
let myColor: ColorPalette = "red-500"; // ✅
let myColor2: ColorPalette = "blue-900"; // ✅
let bad: ColorPalette = "green-1000"; // ❌
三个颜色 × 九个色阶 = 27 种组合,一个类型全部覆盖16。 注意报错只会说「"green-1000" 不是 ColorPalette」——它不会告诉你合法值有哪些; 想知道合法拼写,得悬停 ColorPalette 看展开的 27 个成员。这又是「悬停即输出窗口」: 模板字面量类型生成的并集,只有悬停能完整看到16。
设计系统里的 token 名(token 是设计系统里给颜色、间距这类「设计变量」起的名字,
如 color-primary、spacing-4)、路由表、事件名——凡是「枚举词 + 固定格式」的字符串,
都可以这样一次性声明完。对调用方,它是个普通类型:该报错处报错、该补全处补全
(myColor = " 触发补全时,编辑器列得出 27 个候选),和手写 27 个成员的并集零差别。
但原书在这里放了全书最直白的警告之一: 这些类型会「组合爆炸」—— combinatorially explosive——生成巨大的并集。从路由到资源标识符到十六进制码都能用它查, 构造复杂字符串字面量类型时务必小心17。记住一个经验值:并集成员相乘,几十个乘数就上万。
它查得了格式,查不了内容¶
模板字面量类型的能力边界要划清,免得用错地方:
type HexColor = `#${string}`;
let a: HexColor = "#ff0000"; // ✅ 以 # 开头
let b: HexColor = "ff0000"; // ❌ 没 #,拦住了
let c: HexColor = "#gggggg"; // ✅ 照样通过!
第三行暴露边界:${string} 只承诺「这里是任意字符串」,它不知道 g 不是十六进制字符。
类型系统匹配得了「形状」,验证不了「取值」——「恰好六个十六进制字符」这种约束
需要比 ${string} 细得多的拼接,超出本章与原书的范围。
实用结论:模板字面量类型适合「固定前后缀 + 开放中段」(路由、扩展名、命名前缀);
凡是对中段字符本身有要求的(字符表、位数),别指望它。
这一节是从 ${string} 的定义直接推出来的(补充,不在书里,来自通用知识)。
四个内置的字符串工具类型¶
对字符串类型做大小写变换的内置工具,原书顺手列了四个18:
type A = Uppercase<"hello">; // "HELLO"
type B = Lowercase<"HELLO">; // "hello"
type C = Capitalize<"matt">; // "Matt"(首字母大写)
type D = Uncapitalize<"PHD">; // "pHD"(首字母小写)
单独用的机会不多,但它们是第 7 节「键重映射」的重要零件
(get${Capitalize<K>} 这种写法离开 Capitalize 就拼不出来)。
原书对它们的评语是:它们证明了 TypeScript 类型系统的灵活程度18。
5. 第三件工具:条件类型——类型里的 if/else(备而不用)¶
先把丑话说在前面,这是原书自己的定位: 条件类型主要在库的场景里、对付真正复杂的代码时有用;应用代码里很少需要。 这一节的存在意义是「万一撞见,认得出来」19。
它解决什么样的问题?造一个 ToArray<T>,把任意类型变成数组类型:
type ToArray<T> = T[];
type A = ToArray<string>; // string[] ✅
type B = ToArray<string[]>; // string[][] ❌ 传进去的已经是数组了!
第二行暴露了缺陷:传数组进来,得到的是「数组的数组」。 真正想要的是「如果已经是数组就原样返回,否则包一层」——这正是 if/else, 而条件类型的语法就是 JavaScript 的三元运算符:
拆解这三段(照原书的拆法)20:
条件怎么读?把它想象成一次「赋值测试」。
T extends any[] 问的是:T 这个类型,能不能赋给「期待 any[] 的函数」的形参?
—— 就是问「T 是不是某种数组」。想测是不是字符串就写 T extends string21。
测试通过走问号后的真分支,不通过走冒号后的假分支:
type A = ToArray<string>; // string 不是数组 → string[]
type B = ToArray<string[]>; // string[] 是数组 → 原样返回 string[] ✅
调试条件类型靠的还是悬停:把实参代进去,悬停整个类型别名,看「条件求值」之后 落在真分支还是假分支——它就是类型层面的单步执行。真要在应用代码里写, 建议先把期望值写成注释,悬停对答案,再迭代条件本身20。
原书给条件类型的总评,一句话里有两个「极其」:
它把 TypeScript 的类型系统变成了一门完整的编程语言;极其强大,也极其复杂。
应用代码里极少需要,库代码里能创造奇迹22。本章的练习刻意只碰它一次
(StrictOmit 的约束写法里那个 extends keyof T 是它唯一一次露面),
往深里学它不是这本书的目标——记住「认得出来、别在应用里主动用」就够了。
如果只在脑子里留一个画面,就留这个:泛型类型是函数,映射类型是循环, 模板字面量类型是字符串拼装,条件类型是分支——四句话拼出「类型即程序」的完整图景, 而程序该有的纪律(参数节制、先写后抽象、复杂度预算)在这里一样适用。
6. 第四件工具:映射类型——批量改写形状的键和值¶
从一个具体愿望出发¶
有了唱片接口,你想要一个「所有属性都可选、且可空」的变体:
interface Album {
name: string;
artist: string;
songs: string[];
}
// 目标形状:
type AlbumWithNullable = {
name?: string | null;
artist?: string | null;
songs?: string[] | null;
};
手写当然可以,但 Album 每加一个属性,这里就得跟着改一遍。
Partial 能解决「可选」,解决不了「可空」。映射类型出场:
对一个类型的每个键各做一次变换,生成新对象类型23。
四步走通(每一步都悬停看一眼)¶
第 ① 步:把「遍历键」的骨架写出来。 语法长得像索引签名,但方括号里不是
[k: string],而是 [K in keyof Album]——
in 表示「对 keyof Album 的每个键各迭代一次」,K 是你在循环里给当前键起的名字
(起什么都行)24:
type AlbumWithNullable = {
[K in keyof Album]: K;
};
// 悬停 → { name: "name"; artist: "artist"; songs: "songs"; }
把四步走通的全过程压成一张表,每一行都是「写一行 → 悬停看到什么」:
| 步 | 写下的值位置 | 悬停看到的形状 |
|---|---|---|
| ① | K |
name: "name"——属性类型变成了键名自己(字面量) |
| ② | Album[K] |
name: string——原样复刻了 Album |
| ③ | Album[K] \| null |
name: string \| null——每个属性都可空 |
| ④ | Album[K] \| null + 键后 ? |
name?: string \| null \| undefined——可选生效 |
注意 ④ 的悬停结果里多了 undefined:可选属性的类型自动并上 undefined——
这是第 04 章「可选属性」的老朋友,在映射的语境里又出现了一次26。
第 ② 步:用 K 去索引原类型,取出原属性的类型。
Album[K] 就是索引访问——第 11 章的工具——在这里的意思是「Album 里键 K 对应的那个类型」:
type AlbumWithNullable = {
[K in keyof Album]: Album[K];
};
// 悬停 → { name: string; artist: string; songs: string[]; }
到这一步,你已经用映射类型原样复刻了 Album。这说明映射类型的最小能力就是「复制」, 变换是在复制之上加的25。
第 ③ 步:加变换——每个属性的类型后面接 | null:
type AlbumWithNullable = {
[K in keyof Album]: Album[K] | null;
};
// 悬停 → { name: string | null; artist: string | null; songs: string[] | null; }
第 ④ 步:在键后面加 ?,让每个属性可选:
type AlbumWithNullable = {
[K in keyof Album]?: Album[K] | null;
};
// 悬停 → { name?: string | null | undefined; ... }
目标形状达成,而且 Album 再加属性,这里自动跟上26。
照例包成泛型:Nullable<T>¶
「设计类型」这一章的纪律,就是把每个有用的形状收编成泛型类型:
T 是遍历的对象类型,keyof T 和 T[K] 都随着实参变。一行定义,
团队里任何「可空版」的需求都解决了。原书对映射类型的定位:
变换对象类型极其有用的方法,在应用代码里有各种各样的用处27——
它不像条件类型那样被划进「库专用」。
拿 Nullable 和内置 Partial 并排看一眼,能对「映射类型的日常价值」更有体感:
type Partial<T> = { [K in keyof T]?: T[K] }; // 只加「可选」
type Nullable<T> = { [K in keyof T]?: T[K] | null }; // 「可选」+「可空」
两行的差别只在值位置——这正是映射类型的手感:改形状是声明式的、局部的改动, 想动哪一层(键、可选性、值的包装)就动哪一层,其余原样保留。
改键名:as 重映射¶
变换值不够,还想改键?比如把 Album 的键全变大写。直觉的写法会撞墙:
type AlbumWithUppercaseKeys = {
[K in Uppercase<keyof Album>]: Album[K];
// ❌ Type 'K' cannot be used to index type 'Album'.
};
为什么错?Uppercase 之后,K 已经是 "NAME"、"ARTIST"——变换后的键在原类型里不存在,
Album[K] 索引不到任何东西。需要的恰恰是「键对外用新名字、对内还能按原名索引」。
as 就是干这个的:
type AlbumWithUppercaseKeys = {
[K in keyof Album as Uppercase<K>]: Album[K];
};
// 悬停 → { NAME: string; ARTIST: string; SONGS: string[]; }
as Uppercase<K> 的意思是:循环变量 K 本身保持原样(所以 Album[K] 可用),
但生成的新键名用 Uppercase<K> 的计算结果。
原书特意提醒:这里的 as 跟类型断言的 as 完全是两码事,只是同一个关键字的复用28。
对并集做映射¶
映射类型的 in 后面不一定非得是 keyof——任何并集都行:
type Example = "a" | "b" | "c";
type MappedExample = {
[E in Example]: E;
};
// 悬停 → { a: "a"; b: "b"; c: "c"; }
并集的每个成员各变成一个键,值就是成员自己29。 这把「并集 → 对象」的转换在工具类型的世界里非常常用。
7. 九个练习,各教一件事¶
原书本章的练习不是重复劳动,每一步都在逼你用刚学的工具解决一个「像真需求」的问题。 这一节按「模具 → 默认值与约束 → 约束补检查 → 字符串 → 映射组合拳」的顺序过一遍, 每个练习都只取它独一无二的那一课,和正文重复的解法不再重复。
DataShape:泛型类型的最小可用形态(练习 15-1)¶
需求:API 响应形状 UserDataShape、PostDataShape 都是「要么 data、要么 error」,
error 那半边一字不差,只有 data 不同。模具如下:
type DataShape<TData> =
| { data: TData }
| ErrorShape;
type UserDataShape = DataShape<{ id: string; name: string; email: string }>;
type PostDataShape = DataShape<{ id: string; title: string; body: string }>;
要点:并集里不动的半边原样保留,只参数化变化的那半边30。 这是泛型类型最典型的用法——判别联合的公共骨架。
值得多看一眼的是为什么 ErrorShape 能原样共享:它没有任何「随资源而变」的部分,
一个字都不用参数化。模具的参数永远只该覆盖真正变化的位置;
把不变的也参数化(比如 DataShape<TData, TError>)不是错,但每多一个参数,
就多一份「实参传什么」的决策负担——第 ② 步 TMetadata 的教训在这里提前生效8。
PromiseFunc:给函数类型加参数(练习 15-2)¶
type PromiseFunc = (input: any) => Promise<any>;
// 要变成:PromiseFunc<string, string> → (input: string) => Promise<string>
解法两步走,每步都有可悬停的验证点。
第 ① 步:加类型参数。 type PromiseFunc<TInput, TOutput> = (input: any) => Promise<any>;
调用处的报错(「PromiseFunc 不是泛型」)立刻消失——模具开始收参数了;
但悬停调用处,返回类型还是 any 味道的 Promise31。
第 ② 步:把 any 逐个换成参数。
type PromiseFunc<TInput, TOutput> = (input: TInput) => Promise<TOutput>;
两个测试各自悬停:PromiseFunc<string, string> 展开成
(input: string) => Promise<string>,PromiseFunc<boolean, number> 展开成
(input: boolean) => Promise<number>——和期望类型逐字相等31。
这个练习的价值在于:泛型类型不只服务于对象形状,也服务于函数类型——
类型参数可以出现在「返回的函数」的任意位置(参数位、返回位、Promise 的实参位)。
学完本章回头看,第 05 章的 Set<string>、第 07 章的 Pick<T, K> 都是同一族:
别人造的泛型类型,本章教你自己造。
Result 类型:默认值与约束的合奏(练习 15-3、15-4)¶
一个「成功或失败」的结果类型,开始时两个参数都要传:
type Result<TResult, TError> =
| { success: true; data: TResult }
| { success: false; error: TError };
const createRandomNumber = (): Result<number> => { /* ... */ };
// ❌ Generic type 'Result' requires 2 type argument(s)
练习 15-3 教默认值: 错误几乎都是 Error 类型,那就让它默认:
Result<number> 从此合法。Result 模式的收益原书点了一句:
拿到 result 之后必须先检查 success 才能摸 data——
它比 try...catch 更能保证错误被处理32。
这个模式值得单独记住,理由原书也给了:Result 类型是 「确保错误被妥善处理的好办法」,可以成为 JavaScript 里 try...catch 之外的 另一种选择32。判别联合(第 06 章)+ 泛型默认值(本章)两件旧工具拼出一个新范式: 错误从「会炸出来的异常」变成「类型系统逼你检查的一个分支」。 这就是本章教法的缩影——新能力几乎从不引入新机制,而是旧机制的组合。
练习 15-4 教「默认值 + 约束」叠加。 测试要求:传字符串当 TError 必须报错,
传 Error、TypeError、{message: string; code: number}、甚至省略——都要通过。
一个签名同时满足:
约束(extends)挡住 {message: string} 之外的东西,默认值(= Error)处理「不传」,
两个机制各管一段,互不干扰33。
还附带一个第 13 章世界观的印证:传 {id: string}, {message: string; code: number}
算「开放约束」的合法超额——有 message 就行,多出来的 code 照收33。
StrictOmit:约束把「拼错的键」变成编译错误(练习 15-5)¶
Omit<{a: string}, "b"> 不会报错——想删掉一个不存在的键,TypeScript 一声不吭。
能不能造一个「键写错就报错」的 StrictOmit?
起点有个提示性的报错:
type StrictOmit<T, K> = Omit<T, K>;
// ❌ Type 'K' does not satisfy the constraint
// 'string | number | symbol'.
Omit 内部要求第二个参数是键的并集,K 完全不受限,所以不合。 解法分两步走,每步都有看得见的进展:
第 ① 步:先写死,验证机制。 K extends "a"——ShouldFail 里的 "b" 立刻报错
(Type '"b"' does not satisfy the constraint '"a"')。机制对了,但写死的键没用34。
第 ② 步:把约束换成动态的。 K extends keyof T:
K 被绑死在「T 的键」上,传 T 没有的键就是约束违反。
三个测试全过:正常省略、整个 omit 成 {}、传 never(空并集,什么都不删)35。
这个练习是「约束即文档」的最佳演示:类型参数上的 extends 不只是拦错, 它把「第二个参数是什么意思」直接写进了签名。
AbsoluteRoute:模板字面量类型的实战(练习 15-6)¶
type AbsoluteRoute = string;
goToRoute("/home"); // 想让它合法
goToRoute("somewhere"); // 想让它报错(现在不报,因为它是 string)
第一次尝试 `/${"home" | "about" | "contact"}`——测试过了,但路由被写死成三条。
把插值位置换成 string:
「任何以斜杠开头的字符串」就此成立,"somewhere" 被拦36。 这是模板字面量类型最朴素的威力:一个字符的格式约束,不用正则、不用运行时。
Sandwich:并集 × 模板的组合生成(练习 15-7)¶
三种面包 × 三种馅,要求一个类型枚举全部 9 种三明治名:
type BreadType = "rye" | "brown" | "white";
type Filling = "cheese" | "ham" | "salami";
type Sandwich = `${BreadType} sandwich with ${Filling}`;
一行。生成结果正是 9 个成员的并集——原书把展开结果完整列了出来:
"rye sandwich with cheese" "brown sandwich with cheese" "white sandwich with cheese"
"rye sandwich with ham" "brown sandwich with ham" "white sandwich with ham"
"rye sandwich with salami" "brown sandwich with salami" "white sandwich with salami"
图说:3 × 3 的乘法表,模板字面量类型替你把整张表打了出来[^37]。
练习 15-6 是「宽插值」(${string}),这题是「窄插值」——插值位置放并集,
组合的乘法就由类型系统替你做完。两题合起来就是模板字面量类型的完整光谱:
能收(限定到枚举组合),也能放(放开到任意字符串)。
AttributeGetters:映射类型 + 索引访问的组合拳(练习 15-8、15-9)¶
需求:把 Attributes 接口变成「每个键对应一个返回该属性类型的函数」:
interface Attributes {
firstName: string;
lastName: string;
age: number;
}
// 目标:
type AttributeGetters = {
firstName: () => string;
lastName: () => string;
age: () => number;
};
练习 15-8 的解法一行:
两个零件各出一份力:[K in keyof Attributes] 遍历键生成属性,
Attributes[K] 取出该键的值类型塞进返回位置38。
原书点明:Attributes[K] 是整题的题眼——索引访问让「键」和「值」在类型层面重新连上了。
练习 15-9 要改键名:把 firstName 变成 getFirstName。 原书故意展示了一次「错的走法」,而且展示的每一步都有悬停结果作证, 值得把两次尝试的推理链完整走一遍——因为「先在外部改键」这个冲动, 每个学映射类型的人都会犯一次。
// 错误尝试:先在外面把键算好
type NewAttributeKeys = `get${keyof Attributes}`;
// → "getfirstName" | "getLastName" | "getAge"…不对,首字母没大写
悬停立刻露馅:模板原样拼出了 "getfirstName"。补上 Capitalize:
type NewAttributeKeys = \get${Capitalize<keyof Attributes>}`;`
悬停 → "getFirstName" | "getLastName" | "getAge",键全对了。
于是顺势写进映射:
type AttributeGetters = {
[K in NewAttributeKeys]: () => Attributes[K];
// ❌ Type 'K' cannot be used to index type 'Attributes'.
};
键被改名之后,循环变量 K 手里攥着的已经是新名字,原类型里查无此键—— 和上一节 Uppercase 的撞墙完全同构。原书点破要害:「You need to maintain access to the original key in the mapped type」——映射循环里必须留住原键的访问权。 正确解法是 as 重映射: 循环照旧按原名走,只在「对外命名」那一刻套用模板:
type AttributeGetters = {
[K in keyof Attributes as `get${Capitalize<K>}`]: () => Attributes[K];
};
// → { getFirstName: () => string; getLastName: () => string; getAge: () => number; }
键是 get${Capitalize<K>} 的计算结果,值仍然按原键 K 索引——
as 让「新名字」和「原索引」在同一次循环里共存39。
这个三件套(映射 + 模板字面量 + 索引访问)是「自造工具类型」的标准配方,
值得整套背下来。
8. 悬停驱动的开发法:本章真正的隐性课程¶
把本章所有代码段连起来看,会发现一个反复出现的动作:每改一行,悬停一次。
- 模具声明成
= unknown,悬停 TContent →「参数已声明但从未读取」,发现模具没接管线4; - 填进骨架后悬停 StreamingPlaylist → 看到和原手写一模一样的形状,确认抽象无损5;
- 加了默认值后悬停 → 看到 metadata 推断成
{},确认默认值生效10; - 映射类型四步走,每一步都悬停一次,看
name: "name"怎么变回string、 又怎么变成string | null | undefined2526。
这不是排版习惯,而是使用类型系统的工作方式本身。类型的计算结果不会打印到任何地方, 悬停是它唯一的「输出窗口」;写复杂类型不是先想对再写,而是写一个猜想、悬停看结果、 按结果修正,像用一种能即时反馈的 REPL(读取-求值-打印-循环的交互式环境,敲一行出一个结果)。 原书从头到尾没有把这立成一条规矩,但每一道例题都是这么做的—— 读到后面章节的练习题解法里出现「悬停 shows」时,要知道那不是给答案配图, 那就是解法的推导过程本身。
报错信息是另一半输出窗口¶
悬停看「算出来的结果」,报错看「哪里没算成」——本章每种工具都有自己的报错方言, 提前认一遍,真撞上时不慌:
| 报错文本 | 出现在哪种工具 | 它在告诉你什么 |
|---|---|---|
Type 'TContent' is declared but its value is never read |
泛型类型(定义处悬停参数) | 模具收了参数没接进骨架4 |
Generic type 'ResourceStatus' requires 2 type argument(s) |
泛型类型(调用处) | 类型实参没传够,考虑给参数加默认值9 |
Type '...' does not satisfy the constraint 'HasId'. Property 'id' is missing... |
约束(调用处) | 实参缺了约束要求的属性,报错直接列出缺哪个12 |
Type 'K' cannot be used to index type 'Album' |
映射类型/键重映射 | 循环变量被改过名,原类型里查无此键——该用 as28 |
Type '"b"' does not satisfy the constraint '"a"' |
约束(写死版 StrictOmit) | 机制已通,把写死的约束换成 keyof T 就完工34 |
这张表有个共同规律:类型系统的报错几乎都是「结构性」的——它报的是 「缺哪个属性、哪个键索引不到、哪个约束不满足」,而不是「你错了」。 读的时候把它当成编译器在描述它看到的形状,顺着描述去找形状差在哪,九成报错都能自行化解。 (「方言」的归纳是我们的说法,具体报错文本逐字来自原书各段。)
判断(我们的,不是书里的): 「先悬停、再下笔」这条隐性课程,比本章任何一个 具体工具都值得先教给新人。新手写泛型的典型死法是闭眼堆尖括号、对着报错干瞪眼; 而本章的每一步都示范了「把不确定交给悬停」这条出路。 如果错,会错在: 悬停显示的推断结果因 IDE 而异(不同的语言服务器版本、 不同的展示截断策略),个别复杂类型看到的可能是缩写形式;但「悬停即输出窗口」 这个工作方式本身是所有编辑器共通的。
9. 什么时候该造模具,什么时候别造¶
本章教的是「怎么造」,但真到代码评审里,更重要的问题往往是「这团重复值不值得抽象」。 原书没有单列这一节,它的答案散在练习题的选择里,拼起来是这样一张表:
| 重复的样子 | 本章的对策 | 理由 |
|---|---|---|
| 两个类型共享骨架、只有一处类型不同 | 泛型类型(练习 15-1 的 DataShape) | 重复的源头是「形状」,参数化一处就能消掉 |
| 函数类型到处复制、只是入出类型不同 | 泛型函数类型(练习 15-2 的 PromiseFunc) | 同理,参数是「函数的形状」 |
| 字符串值必须符合格式 | 模板字面量类型(练习 15-6) | 运行时查不了的事,编译期一行解决 |
| 枚举词的组合名(设计系统 token、事件名) | 模板字面量 × 并集(练习 15-7) | 组合的乘法是机械的,该交给类型系统 |
| 对象形状的批量改写(全可空、全只读、getter 化) | 映射类型(练习 15-8) | 遍历 + 索引访问是声明式的,比手写稳 |
| 内置工具「静默放行」了想拦的错误 | 约束补检查(练习 15-5 的 StrictOmit) | 把语义写进签名,调用点当场报错 |
| 写复杂类型时不确定结果对不对 | 悬停驱动:写一个猜想、悬停看展开、按结果修正 | 类型没有 print,悬停是唯一的输出窗口 |
| 想给类型加 if/else 分支 | 条件类型 | 原书划界:库代码的工具,应用里先想想有没有更笨的办法22 |
这张表的上半部分是「该造」,最后一行是「别急着造」。 判据可以浓缩成一句:抽象的支点必须是「已经存在的重复」,而不是「想象中的复用」。 两个类型只出现一次、将来也未必有第三个,就让它重复——模具自身的理解成本 高于它省掉的那几行。
这张表还有一层隐含的意思:本章所有工具都建立在「先看到重复」的前提上。 练习是倒着出的——先给你两个写得差不多的类型,让你「看出哪里在重复」, 然后才是参数化。真到自己的工作里,顺序也一样:先让重复存在一会儿, 等第三、第四个副本冒出来、重复的形态稳定了,再动手造模具。 一上来就抽象,十有八九会把偶然相似误当本质重复,造出来的模具只套得住最初两个例子。
10. 这一章在全书链条中的位置¶
本章是第 11 章「推导」的乘法版:第 11 章教你从现成的类型里取东西
(keyof、typeof、索引访问),本章教你造出能反复产类型的机器
(泛型类型是函数,映射类型是循环,条件类型是分支,模板字面量是字符串拼装)。
原料与机器的关系:没有 keyof 和 T[K],Nullable 和 AttributeGetters 都写不出来。
它也是「设计哲学」一以贯之的体现:TypeScript 的类型系统被刻意做成 一门可以编程的语言——条件类型那句「把类型系统变成完整编程语言」不是修辞, 而是对「类型层面可以跑通用计算」这个事实的陈述22。 但原书同样刻意地给它划了使用边界:应用代码里,泛型类型、映射类型、模板字面量 都是日常工具;条件类型是库的工具。复杂度预算这一课,在第 16、17 章还会再讲一遍。
和第 07 章的对话:Pick 们其实自己就是映射类型¶
学完映射类型,值得回头重看第 07 章的四个内置工具(Partial、Required、Pick、Omit)。 它们不是语言内建的黑魔法——它们本身就是用本章的机制写出来的普通类型定义, 住在 TypeScript 自带的类型声明文件里。举两个对得上的例子:
// Partial 的近似实现(示意,非逐字源码):
type Partial<T> = {
[K in keyof T]?: T[K];
};
// Pick 的近似实现(示意):
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
看 Partial:整个身体就是一个映射类型,键后加 ?、值原样——就是本章第 ⑥ 节第 ④ 步
做的事,一行而已26。看 Pick:它的第二参数带着 extends keyof T 约束——
和练习 15-5 的 StrictOmit 是同一个手法35。
补充(不在书里,来自通用知识): 上面的实现是示意,真实 lib 定义还处理了
映射修饰符等细节,但骨架与本文所示相同。
这个认知的价值在于:本章之后,你读任何内置工具的文档都有底气——
它们全是「本章内容的组合」,不再有「库作者才能懂」的类型。
本章术语卡¶
| 术语 | 一句话 | 首次出现 |
|---|---|---|
| 泛型类型 | 收类型实参、返回类型的「类型函数」 | §2 |
| 类型参数 | 尖括号里的声明,如 <TContent> |
§2 |
| 类型实参 | 用的时候传进去的类型,如 ResourceStatus<{...}> |
§2 |
| 类型参数默认值 | TMetadata = {},不传实参时的兜底 |
§2 第⑤步 |
| 类型参数约束 | TContent extends HasId,开放性的下限 |
§2 第⑥步 |
| 模板字面量类型 | 反引号字符串类型,可插值类型 | §4 |
| 条件类型 | T extends X ? A : B,类型的 if/else |
§5 |
| 映射类型 | [K in keyof T],遍历键生成对象类型 |
§6 |
| 键重映射 | as 在映射里改键名、留原索引 |
§6 |
这张卡也是出门用词表:这些词在别的 TypeScript 文档、库的 README、报错信息里 都会原样出现, worth 记英文名(type parameter / type argument / mapped type / template literal type / conditional type)。
11. 作者的判断与证据(分开标)¶
书里有据的:
- 泛型类型 = 类型函数,收类型实参、返回类型,用处与运行时函数同构6。
- 类型参数默认必传、可用 = 给默认值、可用 extends 加约束——三者都是可验证的语言规则91011。
- 约束是开放的——超出的属性照收,原书明写 open, not closed13。
- 模板字面量类型与并集组合会组合爆炸——原书用了「tread carefully(务必小心)」的措辞17。
- 条件类型「应用代码极少需要」——原书的明确取舍22。
- as 在映射类型里与断言「完全不同的用法」——原书原话28。
原书没说的(我们补):
- 「类型层面的 DRY」这个说法是对原书「reducing repetition」的概括,术语本身是我们的归纳。
- 「经验值:并集成员相乘,几十个乘数就上万」是为演示编的量级感,原书只给了 27 组合这一个真实例子16。
- 「先悬停、再下笔」作为方法论是我们的提炼;原书只示范、不总结(第 8 节已展开)。
- Nullable 与 Partial 的并排对照是我们的教学处理,原书分别出现在不同章节(第 07 章与第 15 章)。
判断(我们的,不是书里的): 本章三样工具的难易排序与上手顺序不是原书的排列顺序。 实际写代码时,最先该掌握的是映射类型——
[K in keyof T]的「遍历 + 索引访问」直觉 一旦建立,Nullable、AttributeGetters 这类需求都能顺手解掉; 其次是模板字面量类型,约束字符串格式是低投入高回报; 泛型类型(本章的主角)反而要最难下决心用,因为它要求你先看出「哪里在重复」—— 这个眼光本身比语法难。 如果错,会错在: 如果团队的代码库高度依赖「API 响应形状」这类天然重复的模式 (练习 15-1 那种),泛型类型的回报期会短得多,上面的排序就不成立—— 难度排序受制于重复的结构化程度,不是普适常数。判断(我们的,不是书里的): 练习 15-5(StrictOmit)是本章最值得收藏的一个成果—— 它示范了一种通用策略:发现内置工具「静默放行」了某类错误时,用类型参数的约束 把检查补回来。Omit 对不存在的键沉默,就在 K 上加
extends keyof T; 同理可以造「键必须存在」的 Pick、「值必须可序列化」的 Record。 约束即校验,这是泛型类型在应用代码里性价比最高的用法,比重型条件类型实用得多。 如果错,会错在: 如果未来 TypeScript 给内置工具加上这类严格变体(官方 StrictOmit), 自造版本就该退役;另外约束写得太紧(比如要求「恰好这些键」)会和 开放对象的世界观打架,实践中「约束太严导致处处报错」和「没有约束」一样糟。判断(我们的,不是书里的): 三样工具的被低估程度排序,和原书给的篇幅成正比反着来—— 原书笔墨最多的是泛型类型,但我们估计日常回报最高的是模板字面量类型: 它解决的问题(字符串格式)在应用代码里到处都是,而成本只有一行反引号; 映射类型次之(需求集中在「批量改写形状」的少数场景); 泛型类型反而是「看天吃饭」——没有结构化重复的地方,它毫无用武之地。 学习时间的分配建议照这个序,而不是照原书的章节顺序。 如果错,会错在: 这个排序严重依赖代码库的类型。API 密集型的项目 (大量响应形状、大量 DataShape 式重复)里泛型类型的回报会冲到第一; 所以更准确的版本是「按你的代码库里哪种重复最多,就先学哪样」。
12. 边界与局限¶
- 组合爆炸是真实的风险,不是客套话。 ColorPalette 只有 27 个成员; 两个各 50 成员的并集相乘就是 2500 个成员的联合类型,IDE 补全会变慢、报错信息会很长。 模板字面量类型最适合「格式 + 小枚举」,不适合「格式 + 大枚举」。
- 条件类型的边界就是「别在应用代码里用」。 原书只给了最简形态; 真实库里的条件类型往往嵌套分配性、推断(infer)等机制,超出本书范围—— 真撞上了,本章的知识足够让你认出「这是条件类型,我该去读库的文档」。
- as 多义性是一个持续的阅读障碍:断言 as(第 12 章)、重映射 as(本章)、
还有 import 里的
import x as y。同一个词三处意思,读别人的类型代码时逐处确认。 - 映射类型不递归:Nullable<T> 只把顶层属性变可空,嵌套对象里的属性不受影响—— 和 Readonly、Partial「只一层深」是同一个家族特性(第 08 章已领教过)。
- 默认值与约束的组合有顺序依赖:
TError extends {message: string} = Error里, 默认值 Error 必须满足前面的约束——写反了(先有默认值再约束)或默认值不合约束, 报错。这是「约束优先、默认兜底」的记忆钩子33。 keyof作用于联合类型不是本章的主题,但值得防一手:keyof (A | B)取出的是 两个类型共有的键(并集的键是交集),不是「任意一边的键」。 写约束时如果 T 可能是联合,先想清楚要的是哪一份(补充,不在书里,来自通用知识; 原书本章的 TContent 都是对象类型,没踩这个坑)。
13. 可带走的¶
- 泛型类型 = 类型函数:尖括号里是参数声明,用时传类型实参;参数、默认值(=)、约束(extends)与函数同名同义。
- 用泛型类型的所有实参默认必传;泛型函数不传能推断(下一章)——别把这俩搞混。
- 约束是开放的:带够要求的属性就行,多出来的照收;想「恰好」得另想办法。
- extends 至少四个意思(泛型约束、类继承、interface 继承、条件类型),读到先问是哪一个。
- 模板字面量类型:
`${string}.png`让字符串格式进编译期;插并集 = 组合相乘。 - 组合爆炸是真的:并集成员相乘,别对大枚举套模板字面量。
- 条件类型:
T extends X ? A : B,三元运算符的语法,「能不能赋给」的语义;库的工具,应用慎用。 - 映射类型四步:
[K in keyof T]遍历 →T[K]索引取值 → 值变换 → 键后加?;照例包成泛型(Nullable<T>)。 - 改键名用
as:对外新名字、对内原键索引;「先在外面改好键」的写法必然撞「K 不能索引 T」。 - AttributeGetters 三件套(映射 + 模板字面量 + 索引访问)是自造工具类型的标准配方,背下来。
- 练习 15-5 的策略值得推广:内置工具静默放行时,用约束把检查补回来(StrictOmit)。
- 类型参数越多类型越复杂——和函数一样;默认值和约束能把复杂度压回去。
这 12 条里,前 4 条管「看懂别人的泛型」(读库的类型签名、读报错), 中间 4 条管「写出自己的泛型」(映射四步、as 重映射、三件套、约束补检查), 后 4 条管「别过度设计」(组合爆炸、条件类型划界、模具时机、参数节制)。 分这三层记,比按顺序背更能对应真实工作里的用场。
另外留一个「下次能用」的入口:遇到「两个类型好像」的时刻,回来翻第 9 节那张 「什么时候该造模具」的表;遇到「字符串值不合法却没报错」的时刻,翻到第 4 节的 AbsoluteRoute——这两个时刻,是本章工具在真实代码里最高频的上岗机会。
最后交代一句本章的读法:正文按「模具 → 字符串 → 分支 → 遍历」四件工具推进, 练习按「从易到难」重排了一遍——两条线互相印证,但都不需要一次读完。 第一遍只读第 1、2、6 节和 AttributeGetters 一段,就够应付日常; 其余各节留作遇到具体问题时回来查的参考。
14. 原文地图¶
本章所有「原书位置」均指原书第 15 章(Designing Your Types)的转码文本;
行号是 library/total-typescript-essentials/text/31-fm-generic-types.txt 的行号,
搜词逐字取自原文,可用 node scripts/book-verify.mjs total-typescript-essentials 回核。
「同上」指上一行的同一原书章(Generic Types)。
| 主题 | 原书位置 | 原文位置 |
|---|---|---|
| 「类型函数」总定义 | Generic Types | text/31-fm-generic-types.txt:16(搜「type function」) |
| 主线例子:两个流式资源类型 | 同上 | text/31-fm-generic-types.txt:21(搜「StreamingPlaylist」) |
| 类型参数声明(= unknown) | 同上 | text/31-fm-generic-types.txt:60(搜「ResourceStatus<TContent> = unknown」) |
| 「参数已声明但从未读取」 | 同上 | text/31-fm-generic-types.txt:85(搜「never read」) |
| 参数填入骨架、两个类型重写 | 同上 | text/31-fm-generic-types.txt:91(搜「type ResourceStatus<TContent>」) · text/31-fm-generic-types.txt:103(搜「ResourceStatus<{」) |
| 第二个类型参数 TMetadata | 同上 | text/31-fm-generic-types.txt:141(搜「TMetadata」) |
| 实参必传报错 | 同上 | text/31-fm-generic-types.txt:192(搜「requires 2 type argument」) |
| 默认类型参数 = {} | 同上 | text/31-fm-generic-types.txt:202(搜「TMetadata = {}」) |
| 约束 extends object / HasId | 同上 | text/31-fm-generic-types.txt:245(搜「extends object = {}」) · text/31-fm-generic-types.txt:252(搜「HasId」) |
| 约束违反的报错文本 | 同上 | text/31-fm-generic-types.txt:284(搜「does not satisfy the constraint」) |
| 「开放的,不是封闭的」 | 同上 | text/31-fm-generic-types.txt:292(搜「open, not closed」) |
| extends 的多义吐槽 | 同上 | text/31-fm-generic-types.txt:306(搜「annoying habits」) |
| PngFile 模板字面量类型 | 同上 | text/31-fm-generic-types.txt:314(搜「PngFile」) · text/31-fm-generic-types.txt:320(搜「my-image.jpg」) |
| ColorPalette 27 种组合 | 同上 | text/31-fm-generic-types.txt:334(搜「ColorPalette」) · text/31-fm-generic-types.txt:340(搜「27 possible combinations」) |
| 组合爆炸警告 | 同上 | text/31-fm-generic-types.txt:342(搜「combinatorially explosive」) |
| 四个字符串工具类型 | 同上 | text/31-fm-generic-types.txt:348(搜「UppercaseHello」) · text/31-fm-generic-types.txt:353(搜「CapitalizeMatt」) |
| 条件类型定位(库为主) | 同上 | text/31-fm-generic-types.txt:361(搜「if/else logic」) |
| ToArray 条件类型拆解 | 同上 | text/31-fm-generic-types.txt:375(搜「T extends any[] ? T : T[]」) · text/31-fm-generic-types.txt:396(搜「T extends string」) |
| 「完整的编程语言」评语 | 同上 | text/31-fm-generic-types.txt:405(搜「full programming language」) |
| 映射类型总定义 | 同上 | text/31-fm-generic-types.txt:409(搜「iterating over its keys」) |
| 映射四步:键名变字面量的悬停 | 同上 | text/31-fm-generic-types.txt:430(搜「K in keyof Album」) · text/31-fm-generic-types.txt:444(搜「currently iterated key」) |
| Album[K] 复刻原类型 | 同上 | text/31-fm-generic-types.txt:447(搜「Album[K]」) |
| 可空与可选的逐层叠加 | 同上 | text/31-fm-generic-types.txt:460(搜「Album[K]」) · text/31-fm-generic-types.txt:483(搜「all properties optional and nullable」) |
| Nullable<T> 泛型化 | 同上 | text/31-fm-generic-types.txt:487(搜「Nullable」) |
| as 键重映射(Uppercase) | 同上 | text/31-fm-generic-types.txt:512(搜「as Uppercase<K>」) |
| as 与断言「完全不同的用法」 | 同上 | text/31-fm-generic-types.txt:522(搜「completely different use」) |
| 对并集做映射 | 同上 | text/31-fm-generic-types.txt:533(搜「MappedExample」) |
| 练习 15-1 DataShape | 同上 | text/31-fm-generic-types.txt:586(搜「DataShape<TData>」) |
| 练习 15-2 PromiseFunc | 同上 | text/31-fm-generic-types.txt:634(搜「Promise<TOutput>」) |
| 练习 15-3 Result 默认值 | 同上 | text/31-fm-generic-types.txt:689(搜「TError = Error」) |
| 练习 15-4 约束 TError | 同上 | text/31-fm-generic-types.txt:740(搜「TError extends {message: string}」) |
| 练习 15-5 StrictOmit | 同上 | text/31-fm-generic-types.txt:781(搜「does not satisfy the constraint 'string」) · text/31-fm-generic-types.txt:838(搜「keyof T」) |
| 练习 15-6 AbsoluteRoute | 同上 | text/31-fm-generic-types.txt:889(搜「/${string}」) |
| 练习 15-7 Sandwich | 同上 | text/31-fm-generic-types.txt:940(搜「sandwich with ${Filling}」) |
| 练习 15-8 AttributeGetters | 同上 | text/31-fm-generic-types.txt:999(搜「Attributes[K]」) |
| 练习 15-9 错误尝试与 as 解法 | 同上 | text/31-fm-generic-types.txt:1066(搜「NewAttributeKeys」) · text/31-fm-generic-types.txt:1083(搜「get${Capitalize<K>}」) |
| 本章小结 | 同上 | text/31-fm-generic-types.txt:1101(搜「type functions」) |
-
出处:「Generic Types」第 21–54 段(
text/31-fm-generic-types.txt:21,搜「StreamingPlaylist」)。原文:两个类型都表达「either available with specific content or unavailable with a reason」;唯一差别是 content 结构(第 54 段,搜「title property and an artist property」)。 ↩ -
出处:「Generic Types」第 12 段(
text/31-fm-generic-types.txt:12,搜「composing and transforming types」)。原文点明本章教「generic types」「template literal types」「mapped types」三样。 ↩ -
出处:「Generic Types」第 58–63 段(
text/31-fm-generic-types.txt:58,搜「angle bracket syntax」)。原文:type 参数用尖括号声明;「Often, type parameters are named with single-letter names like T, K, or V, but you can name them anything you like」(第 63 段)。 ↩ -
出处:「Generic Types」第 75–87 段(
text/31-fm-generic-types.txt:85,搜「never read」)。原文悬停提示「Type 'TContent' is declared but its value is never read」;「it's just returning unknown, no matter what is passed in」见第 87 段。 ↩↩↩ -
出处:「Generic Types」第 101–131 段(
text/31-fm-generic-types.txt:103,搜「ResourceStatus<{」)。悬停结果与手写字段逐一相同见第 118–131 段(搜「hovering over StreamingPlaylist shows」)。 ↩↩↩ -
出处:「Generic Types」第 133 段(
text/31-fm-generic-types.txt:133,搜「type function」)。原文:「ResourceStatus is now a generic type. It's a kind of type function」;「useful in all the ways runtime functions are useful」同段。 ↩↩ -
出处:「Generic Types」第 139–181 段(
text/31-fm-generic-types.txt:141,搜「TMetadata」)。metadata 的具体形状见第 154–179 段(搜「creator」)。 ↩ -
出处:「Generic Types」第 183 段(
text/31-fm-generic-types.txt:183,搜「the more parameters」)。原文:「the more parameters you have, the more complex your types can become」。 ↩↩ -
出处:「Generic Types」第 185–194 段(
text/31-fm-generic-types.txt:192,搜「requires 2 type argument」)。原文:「by default, all generic types require their type arguments to be passed in, just like runtime functions」。 ↩↩↩ -
出处:「Generic Types」第 196–239 段(
text/31-fm-generic-types.txt:202,搜「TMetadata = {}」);metadata 推断为 {} 见第 223–237 段(搜「metadata: {}」)。 ↩↩↩ -
出处:「Generic Types」第 241–267 段(
text/31-fm-generic-types.txt:245,搜「extends object = {}」)与第 252–256 段(搜「HasId」)。「TContent must include an id property」见第 267 段。 ↩↩ -
出处:「Generic Types」第 269–288 段(
text/31-fm-generic-types.txt:284,搜「does not satisfy the constraint」)。完整报错文本含「Property 'id' is missing」;「Once the id property is added...the error will go away」见第 288 段。 ↩↩ -
出处:「Generic Types」第 290–292 段(
text/31-fm-generic-types.txt:292,搜「open, not closed」)。原文:「these constraints are open, not closed. You won't get excess property warnings here. Any excess properties you pass in will be added to the type」。 ↩↩ -
出处:「Generic Types」第 294–306 段(
text/31-fm-generic-types.txt:306,搜「annoying habits」)。原文:「One of TypeScript's annoying habits is that it tends to reuse the same keywords in different contexts」。 ↩↩ -
出处:「Generic Types」第 308–321 段(
text/31-fm-generic-types.txt:314,搜「PngFile」)。.jpg 报错文本见第 320 段(搜「my-image.jpg」)。 ↩ -
出处:「Generic Types」第 323–340 段(
text/31-fm-generic-types.txt:340,搜「27 possible combinations」)。「three colors times nine shades」同段。 ↩↩↩ -
出处:「Generic Types」第 342 段(
text/31-fm-generic-types.txt:342,搜「combinatorially explosive」)。原文:「these types can quickly get combinatorially explosive and create enormous unions. So, tread carefully when building complex string literal types」。 ↩↩ -
出处:「Generic Types」第 344–357 段(
text/31-fm-generic-types.txt:348,搜「UppercaseHello」)。「prove how flexible TypeScript's type system can be」见第 357 段。 ↩↩ -
出处:「Generic Types」第 359–361 段(
text/31-fm-generic-types.txt:361,搜「if/else logic」)。原文:「This is mostly useful in a library setting when working with really complex code」。 ↩ -
出处:「Generic Types」第 373–403 段(
text/31-fm-generic-types.txt:375,搜「T extends any[] ? T : T[]」)。三行注释拆解见第 378–386 段。 ↩↩ -
出处:「Generic Types」第 388–396 段(
text/31-fm-generic-types.txt:396,搜「T extends string」)。「imagine it like a function...only arrays can be passed in」见第 388–394 段。 ↩ -
出处:「Generic Types」第 405 段(
text/31-fm-generic-types.txt:405,搜「full programming language」)。原文:「Conditional types turn TypeScript's type system into a full programming language. They're incredibly powerful, but they can also be incredibly complex. You'll rarely need them in application code, but they can perform wonders in library code」。 ↩↩↩↩ -
出处:「Generic Types」第 407–409 段(
text/31-fm-generic-types.txt:409,搜「iterating over its keys」)。原文:「allow you to create a new object type based on an existing type by iterating over its keys and values」。 ↩ -
出处:「Generic Types」第 427–435 段(
text/31-fm-generic-types.txt:430,搜「K in keyof Album」)。原文:「This will look similar to an index signature...instead of [k: string], you use [K in keyof Album]」;「K is a name you've chosen」见第 433 段。 ↩ -
出处:「Generic Types」第 444–455 段(
text/31-fm-generic-types.txt:447,搜「Album[K]」)。「You've now re-created the object type of Album」见第 457 段。 ↩↩ -
出处:「Generic Types」第 457–481 段(
text/31-fm-generic-types.txt:460,搜「Album[K] | null」)与第 470–481 段(搜「string | null | undefined」)。「Now you have a new type that is derived from the Album type」见第 483 段。 ↩↩↩↩ -
出处:「Generic Types」第 485–492 段(
text/31-fm-generic-types.txt:487,搜「Nullable」)。原文:「Mapped types are an extremely useful method for transforming object types and have many different uses in application code」。 ↩ -
出处:「Generic Types」第 494–522 段(
text/31-fm-generic-types.txt:512,搜「as Uppercase<K>」)。「it's a completely different use of the keyword」见第 522 段;索引报错见第 504 段(搜「cannot be used to index」)。 ↩↩↩ -
出处:「Generic Types」第 524–544 段(
text/31-fm-generic-types.txt:533,搜「MappedExample」)。原文:「each member of the Example union becomes both a key and a value」。 ↩ -
出处:「Generic Types」第 546–604 段(
text/31-fm-generic-types.txt:586,搜「DataShape<TData>」)。 ↩ -
出处:「Generic Types」第 606–635 段(
text/31-fm-generic-types.txt:629,搜「TInput」)。「replace the any types with the type parameters」见第 632 段。 ↩↩ -
出处:「Generic Types」第 699–709 段(
text/31-fm-generic-types.txt:699,搜「must be checked for success」)。「a great alternative to try...catch blocks」见第 709 段。 ↩↩ -
出处:「Generic Types」第 711–762 段(
text/31-fm-generic-types.txt:740,搜「TError extends {message: string}」)。「using extends in generics does not restrict you to passing in only the properties defined in the constraint」见第 760–762 段。 ↩↩↩ -
出处:「Generic Types」第 763–834 段(
text/31-fm-generic-types.txt:823,搜「K extends "a"」);报错文本「does not satisfy the constraint '"a"'」见第 832 段。 ↩↩ -
出处:「Generic Types」第 836–849 段(
text/31-fm-generic-types.txt:838,搜「keyof T」)。「K is bound to extend the keys of T」见第 839 段。 ↩↩ -
出处:「Generic Types」第 851–895 段(
text/31-fm-generic-types.txt:889,搜「/${string}」)。「you'd be limited to only those three routes」见第 885 段。 ↩ -
出处:「Generic Types」第 897–952 段(
text/31-fm-generic-types.txt:940,搜「sandwich with ${Filling}」)。九成员完整列表见第 944–952 段。 ↩ -
出处:「Generic Types」第 954–1017 段(
text/31-fm-generic-types.txt:999,搜「Attributes[K]」)。「The Attributes[K] part is the key to solving this challenge」见第 1002 段。 ↩ -
出处:「Generic Types」第 1019–1095 段(
text/31-fm-generic-types.txt:1083,搜「get${Capitalize<K>}」)。「You can't use K to index Attributes because each K has changed」见第 1072 段;悬停结果见第 1090–1095 段。 ↩