类型推导 — keyof、typeof、索引访问与单一事实源
类型推导 — keyof、typeof、索引访问与单一事实源
这一章讲一件事: 别抄写类型,让类型从已有的东西里长出来。 工具是六个(keyof、typeof、索引访问、Parameters/ReturnType/Awaited、Exclude/Extract); 思想是一个——单一事实源:源定义改一处,所有派生类型自动跟上。 最后一块是清醒剂:推导也是耦合,不是所有重复都该消除。
1. 顶层全景:推导工具箱
从什么推导 用什么 得到什么
─────────────────────────────────────────────────────────────
对象类型 keyof Obj 键的并集 "a" | "b"
值 typeof value 值的类型(含推断)
对象类型 + 键 Obj["a"](索引访问) 属性 a 的类型
数组/元组类型 Arr[number] 全部元素的并集
函数 Parameters<typeof fn> 参数元组
函数 ReturnType<typeof fn> 返回类型
Promise Awaited<Promise<T>> T(剥掉 Promise)
并集 Exclude<U, P> / Extract<U, P> 过滤后的并集
图说:主线例子用一个 as const 对象(programModeEnumMap)把前三行走完,
再用一个第三方库函数走后三行。2. 主线例子:一个对象的三趟推导
贯穿本章的对象,是一个「程序模式映射」——键是大写的常量名,值是实际使用的字符串:
export const programModeEnumMap = {
GROUP: "group",
ANNOUNCEMENT: "announcement",
ONE_ON_ONE: "1on1",
SELF_DIRECTED: "selfDirected",
PLANNED_ONE_ON_ONE: "planned1on1",
PLANNED_SELF_DIRECTED: "plannedSelfDirected",
} as const;as const(第 08 章)保证两件事:对象深度只读、每个属性的类型精确到字面量
("group" 而不是 string)。这个对象是后面所有推导的单一事实源1。
第一趟:typeof——从值到类型。
type ProgramModeMap = typeof programModeEnumMap;typeof 在这里是类型上下文里的 typeof:它不看运行时,直接把
「TypeScript 对这个值的类型的理解」提取成类型2。悬停 ProgramModeMap:
type ProgramModeMap = {
readonly GROUP: "group";
readonly ANNOUNCEMENT: "announcement";
/* ...六个只读属性... */
};注意它和运行时 typeof 的并存:typeof programModeEnumMap 写在值的位置
(给变量赋值),JavaScript 语义,结果是 "object";写在类型的位置
(type X = typeof ...),TypeScript 语义,结果是那个对象类型。同一个关键字,
位置决定含义,编译器分得清3。
第二趟:keyof——从类型到键的并集。
type ProgramModeKey = keyof ProgramModeMap;
// "GROUP" | "ANNOUNCEMENT" | "ONE_ON_ONE" | "SELF_DIRECTED"
// | "PLANNED_ONE_ON_ONE" | "PLANNED_SELF_DIRECTED"keyof 把一个对象类型的全部键收成一个字符串字面量并集4。
两趟可以合并成一行惯用语:keyof typeof programModeEnumMap——
「先 typeof 拿到类型,再 keyof 拿键」。这个组合在日常代码里的出现频率极高5。
第三趟:索引访问——从类型到值的并集。
type AllPrograms = ProgramModeMap[keyof ProgramModeMap];
// "group" | "announcement" | "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected"Obj[keyof Obj] = 「全部键各自的值的并集」——一个对象类型的全部值6。
三趟合成一张图:推导是「沿着值与类型的边界搬运」
把三趟并排,本章的核心结构就显形了——推导工具是搬运工,搬运的路线只有三条:
值世界(programModeEnumMap,运行时的真对象)
│ typeof(第 1 趟)
▼
类型世界(ProgramModeMap,类型的镜像)
│ keyof(第 2 趟) │ 索引访问["GROUP"](第 3 趟变种)
▼ ▼
键的并集(ProgramModeKey) 值的并集(AllPrograms)
图说:左边这条线(键)和右边这条线(值)都从同一个类型出发——
键清单和值清单是同一对象的两个投影,各自独立可用,也都从同一事实源推导。这张图解释了为什么本章的工具「学一个会一片」:它们不是八个独立工具, 而是同一个搬运动作的三个方向。typeof 跨界(值 → 类型),keyof 和索引访问 在类型世界内部移动(类型 → 键并集 / 类型 → 值并集)。第 7 节的 Parameters/ReturnType/Awaited 则是把搬运路线延伸到函数和 Promise 上—— 路线变长,动作不变。
还有一个值得点破的性质:三趟推导全部可逆查。任何一处推导结果, 悬停都能看到展开;顺着展开往回走,总能走回 programModeEnumMap 的那六行字面量。 推导链有多长,可逆性就有多长——这是「单一事实源」比「手写副本」优越的 深层原因:副本断了就不知道断在哪,推导链永远能回到源头。 三趟走完,从一个值出发得到了三种类型:完整形状、键的并集、值的并集。 而源头只有 programModeEnumMap 一份——给它加一个新模式,三个派生类型全部自动更新。 这就是本章的报酬结构。
把三趟的悬停结果并排再看一眼,你会发现一个值得注意的事实:
第二趟和第三趟的结果都是字面量并集——"GROUP" | ... 和 "group" | ...——
而不是 string。原因是源上有 as const:推断精度在源头就被锁到字面量,
推导只是把这个精度原样传递(第 08 章「as const 是后续一切的地基」在本章兑现)。
如果忘了 as const,typeof 拿到的是 { readonly GROUP: string; ... },
三趟推导全部退化成 string——推导不会凭空创造精度,它只会搬运源的精度。
诊断「推导结果 unexpectedly 宽」的第一步,永远是回头查源的推断。
3. keyof:键的并集,及它的第一道应用
单独看 keyof 的价值。假设有一个 Album 接口:
interface Album {
title: string;
artist: string;
releaseYear: number;
}
type AlbumKeys = keyof Album; // "title" | "artist" | "releaseYear"Album 加了 genre: string,AlbumKeys 立刻变成四成员的并集——keyof 是引用,
不是快照7。这个并集最常见的用途是给「键参数」定型:
function getAlbumDetails(album: Album, key: AlbumKeys) {
return album[key]; // 索引访问,返回类型随 key 收窄
}
getAlbumDetails(album, "producer"); // ❌ "producer" 不是 Album 的键传一个 Album 没有的键,当场报错。键的有效性和对象类型从此绑定8。
练习 10-1 是这个模式的工程版:一个表单有三个输入(name、email、password),
inputs 变量用 Record<"name" | "email" | "password", {...}> 声明——
键的并集是手抄的,和 FormValues 接口重复。改成 Record<keyof FormValues, {...}>
之后,FormValues 加字段,inputs 的键自动跟上9。手抄并集 → keyof 并集,
是本章最高频的一次重构。
keyof 是引用不是快照:一个必须建立的反应
上面埋了一个值得单独强调的性质:keyof 的结果跟随源类型的当前形状,而不是 声明那一刻的形状。 这在「接口会演化」的现实里意味着:
第 0 天:type AlbumKeys = keyof Album → "title" | "artist" | "releaseYear"
第 30 天:Album 加了 genre 字段
AlbumKeys 此刻(无需任何修改)= "title" | "artist" | "releaseYear" | "genre"
好处:键清单永远新鲜,一次 keyof 永久免费
代价:Album 的每一次加字段,都可能在所有用 AlbumKeys 的地方引发新报错
——「键参数」们的合法性被重新定义了这个「免费的更新 vs 不可控的波及」正是第 9 节两问决策的具体化:keyof 自动传导 更新(问 ① 通过),但如果某些键参数有意只接受旧字段(比如一个只读详情的 UI), 传导就变成了事故(问 ② 该解耦)。keyof 本身无善恶,用在注定同生共变的地方 是神器,用在各自演化的地方是连锁雷管——判断永远落在「这两份是否该同步」上。
还有一道 keyof 的边界题,来自第 5 节的 [number] 技术合流:keyof 数组不是下标。
对数组类型求 keyof,得到的是 "length" | "push" | "pop" | … 的宽并集——
这是 10-6 翻车的根源。想「对数组枚举元素」的正确工具是 [number]
(T[number] 得元素并集),不是 keyof;想「对对象枚举键」才是 keyof。
数组找 [number],对象找 keyof——两个小口诀各管一类,混用就是 29 项的灾难。
4. typeof:从值提取类型,以及它的边界
typeof 的适用面比对象广:对象、函数、类都可以。任何一个运行时的值,
typeof 都能把它的类型世界版本交给你10。但有一个方向是死的:
// ❌ 没有 valueof 这种东西!
const album = valueof Album;只能从值到类型,不能从类型到值。 原因第 03 章就立过:类型运行时不存在, 编译器没法凭空猜你想要的 title 是什么字符串。「value world → type world 的单向门」 是初学者最常问的「为什么不行」,答案就一句:类型是被擦除的,擦除方向不可逆11。
把这个单向门和本章的工具摆在一起,全章的世界观就完整了:
值世界 ════typeof(可)════▶ 类型世界
值世界 ◀════valueof(不存在)═ 类型世界
类型世界 ══keyof / 索引访问 / 工具类型(可)══▶ 类型世界
图说:两个世界之间只有一扇单向门(typeof),进了类型世界,
内部四通八达;但没有任何门能回到值世界——运行时创建值,
只能靠你自己的代码(字面量、new、函数调用)。这个不对称性是「类型擦除」的最深后果,值得和第 13 章的两个世界专题对照着看: 第 13 章讲两个世界如何共存(同名、跨界),这里讲两个世界如何通行(单向)。 合起来才是完整的地图。
练习 10-2 是 typeof + keyof 的标准动作:环境配置对象 configurations
是事实源,Environment 类型从「手抄的三个字面量」改成 keyof typeof configurations——
配置对象加了新环境,Environment 自动多一个成员12。
5. 索引访问类型:类型层面的点号
解决什么问题: 想取 Album 里 title 的类型。直觉写 Album.title——报错:
Cannot access 'Album.title' because 'Album' is a type, but not a namespace.
Did you mean to retrieve the type of the property 'title' in 'Album'
with 'Album["title"]'?报错信息自己给了答案:类型层面访问属性,用方括号 Album["title"],
和运行时 album.title 的方括号版 album["title"] 平行13:
type AlbumTitle = Album["title"]; // string索引访问的三件套:
① 元组取元素。 type AlbumTuple = [string, string, number],
AlbumTuple[0] 是 string,AlbumTuple[2] 是 number——索引是数字14。
② 链式取嵌套。 Album["artist"]["name"] 一路点进嵌套结构15。
③ 喂并集,一次取多个。 这是索引访问最出彩的用法:
type Album = { title: string; isSingle: boolean; releaseYear: number };
// 笨写法:三个索引访问再并起来
type T1 = Album["title"] | Album["isSingle"] | Album["releaseYear"];
// 巧写法:把键的并集喂给一个索引访问
type T2 = Album["title" | "isSingle" | "releaseYear"]; // string | boolean | number16 巧写法的再进一步:Album[keyof Album]——键不要手抄,让 keyof 生成。
「keyof 出键的并集,索引访问收值的并集」这对组合,是提取对象全部值的惯用语6。
练习 10-3/10-4/10-5 把这三招各演一遍:取单个值(ProgramModeMap["GROUP"] 得 "group")、
取部分值(ProgramModeMap["PLANNED_ONE_ON_ONE" | "PLANNED_SELF_DIRECTED"] 得两个
planned 值)、取全部值(ProgramModeMap[keyof ProgramModeMap])17。
[number]:数组的全部元素
解决什么问题: programModes 这次是数组形态(as const 的字符串数组),
想要「全部元素的并集」。照搬对象的 keyof typeof 会出事:
type AllPrograms = (typeof programModes)[keyof typeof programModes];
// 悬停 → "group" | "announcement" | ... | 6 | (() => IterableIterator<...>) | ...
// ↑ 混进了数组的方法类型和长度!数组的键是数字索引加一整个 Array 接口的方法名——keyof 会把 length、map、
迭代器全收进来18。正解是 [number]:数字是所有合法索引的类型,
(typeof programModes)[number] 就是「任意一个元素的类型」的并集19:
type AllPrograms = (typeof programModes)[number];
// "group" | "announcement" | "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected"数组加新元素,并集自动扩大——可扩展性保住了20。
记这个对照:对象的「全部值」用 [keyof Obj],数组的「全部元素」用 [number]。
练习 10-6 的完整解剖:一次失败改良的启示
这道题值得单独走一遍,因为它展示的是「接近正确 → 发现污染 → 找到正解」的探索过程, 而不是一步到位的答案。
尝试一(对象套路照搬): (typeof programModes)[keyof typeof programModes]。
直觉完全合理——前面五道题都这么解的。悬停结果却傻眼:并集里除了六个字符串,
还混进了 6(length 的类型)、(() => IterableIterator<...>)(values 方法的类型)
等三十多项18。原因是数组类型不止「索引 → 元素」这一层,它还继承了 Array
接口的全部成员,而 keyof 一个数组类型时,这些成员名都是键。
尝试二(手动索引并集): 既然键是数字,那就把数字键写全:
(typeof programModes)[0 | 1 | 2 | 3 | 4 | 5]。结果对了,但数组一变长就要手动改——
把「单一事实源」又变回了「两份要同步的清单」,正好违反本章主旨19。
正解([number]): 数字类型是所有合法索引的「类型」,索引访问认它:
(typeof programModes)[number]。数组的元素类型全部取出,方法类型一个不进,
而且数组随便加元素都自动跟进20。
三个尝试各教一件事:照搬套路前先想「这个类型的键空间长什么样」;
手动枚举索引是反 DRY 的倒退;[number] 是数组元素并集的官方通道。
6. as const 枚举:enum 的现代替代,完整对照
第 10 章(原书第 9 章)欠下的那笔账,在这里兑现。三招齐全后,看看 「JavaScript 风格的枚举」长什么样:
const albumTypes = {
CD: "cd",
VINYL: "vinyl",
DIGITAL: "digital",
} as const;
// 键的并集(大写常量名)
type UppercaseAlbumType = keyof typeof albumTypes; // "CD" | "VINYL" | "DIGITAL"
// 值的并集(实际使用的字符串)
type AlbumType = (typeof albumTypes)[keyof typeof albumTypes]; // "cd" | "vinyl" | "digital"
function getAlbumType(type: AlbumType) { /* ... */ }这就是所谓 POJO(Plain Old JavaScript Object)路线:一个普通对象 + 两行类型推导。 「要一点 TypeScript 魔法来 setup,但结果简单易懂」——原书的评价21。 和 enum 的正面对照,三个维度:
① 裸值放行。 AlbumType 是字符串并集,所以 getAlbumType("vinyl") 合法、
getAlbumType("cassette") 报错;enum 则要求必须穿枚举成员的马甲。
POJO 更灵活,enum 更显式——重构成本不同:裸字符串传得到处都是时,改值比改成员名难查22。
② 结构化 vs 名目化。 两个成员一模一样的 enum 互不兼容(第 10 章的 AlbumStatus2 实验); 两个值一模一样的 POJO 完全兼容——「值从哪来不重要」23。名目化能多抓一类 bug (两个概念恰好同值时防止混用),代价是更 restrictive。
③ 编译产物。 POJO 编译后就是一个普通对象,没有任何魔法;enum 有反向映射那些戏法 (第 10 章的三个怪癖)24。
原书的裁决:团队习惯 enum 就用 enum;今天新开项目,用 as const25。 还有一个前瞻注脚:给 JavaScript 加枚举的提案正在进行,而且多半会和 TypeScript 的 enum 长得不一样——又一个「enum 是遗产」的信号26。
迁移路径:存量 enum 怎么换到 as const
「新代码用 as const」之外,存量项目真正的问题是:已经在用的 enum 要不要换、怎么换。 原书没有给迁移手册,这里补一个基于本章工具的渐进路线(我们的建议,不在书里):
第 1 步(零风险):不动 enum,只把「类型位置」换成推导
把函数参数里的 status: AlbumStatus 逐步换成
status: (typeof albumStatus)[keyof typeof albumStatus] 的别名——
调用方继续传 enum 成员(数字/字符串值兼容),类型入口先统一。
第 2 步(低风险):新代码停止产生 enum 成员
新写的代码改用 as const 对象的值(albumStatus.VINYL 是个普通字符串,
传给 enum 参数照样合法——字符串 enum 收裸值的反方向不成立,
但「字符串值传给字符串 enum 参数」是合法的)。
这一步开始,enum 只减不增。
第 3 步(清尾):enum 的最后一个引用消失后,删除 enum 定义本身
全程编译器护航——每一步的报错都是待办清单。
图说:三步的共同点是「每一步结束时代码都是绿的」,
迁移不是大爆炸,是渗流。这条路线的前提是字符串 enum;数字 enum 因为反向映射和裸数字放行的缘故, 和第 3 步的兼容性差一些,迁移时要把数值出现的地方(序列化数据、协议字段)先清点一遍。
判断(我们的,不是书里的): 「as const 对象替代 enum」是 2026 年主流库 (Zod 的枚举、各种 config 常量)的默认写法,但有一个代价原书只轻轻带过: 裸字符串并集在大型代码库里意味着「跳转定义」变弱——
getAlbumType("vinyl")里的 "vinyl" 没有地方可以跳转,而 enum 成员可以。如果团队依赖 IDE 导航做重构, POJO 路线要求纪律:值的使用尽量通过albumTypes.VINYL而不是裸字符串。 如果错,会错在: 一旦 JavaScript 原生枚举提案落地且形态接近 TS 的字符串 enum, 「enum 是遗产」的结论需要重估——原书自己埋了这个伏笔,我们把它挑明。
7. 从函数推导:Parameters、ReturnType、Awaited
对象和数组的推导解决了,轮到函数。三个内置工具,名字即功能:
function sellAlbum(album: Album, price: number, quantity: number) {
return price * quantity;
}
type SellAlbumParams = Parameters<typeof sellAlbum>; // [Album, number, number]
type Price = SellAlbumParams[1]; // number(索引访问取第 2 个参数)
type SellAlbumReturn = ReturnType<typeof sellAlbum>; // number两个细节:
- 函数是值,所以必须先
typeof再喂给工具类型——Parameters<sellAlbum>直接报错,报错信息还会提示你:「是不是想说typeof sellAlbum?」27 - Parameters 的结果是元组(第 05 章)——所以可以对它做索引访问, 也可以对整个元组再推导28。
Awaited 处理异步:ReturnType<typeof fetchUser> 对 async 函数拿到的是
Promise<{...}>(async 函数的返回类型自动套 Promise,第 05 章),Awaited<...>
再剥一层,拿到兑现值的类型29。
为什么需要从函数推导? 自己写的函数,类型随手就有了;问题出在
你不控制的函数——最典型的:第三方库导出了一个函数,却不导出配套类型。
原书给了一个真实案例:@monaco-editor/react 的 Editor 组件,想要复用它的
onMount 回调里 editor 参数的类型,但库里没导出。三步推导把它挖出来:
type EditorProps = Parameters<typeof Editor>[0]; // ① 组件第一个参数(props 对象)的类型
type OnMount = EditorProps["onMount"]; // ② props 里 onMount 的类型
type Editor = Parameters<OnMount>[0]; // ③ onMount 第一个参数的类型三步各用本章的一招(Parameters、索引访问、Parameters),组合出「库里没给、 但编译器知道」的类型。这是推导工具最有实战价值的场景:绕过第三方库的类型缺口, 同时与库的签名保持同步——库升级改了签名,你的派生类型自动跟着变30。
把这个案例的手法抽象一下,它就是一套可复用的「挖类型」流程:
挖一个嵌套的类型,永远问三个问题,从外往里一层层剥:
① 我要的东西在哪个函数的哪个位置?(Parameters<typeof fn>[N] 剥参数)
② 我要的东西在哪个对象的哪个属性?(Obj["key"] 剥属性)
③ 剥出来的是不是还包着一层?(Awaited 剥 Promise)
图说:Monaco 案例是 ①→②→①;你自己的问题按同样的洋葱结构对号入座。练习 10-7/10-8/10-9 是这三个工具的各自演练,其中 10-7 还演示了报错信息
主动教你写 typeof 的暖心瞬间31,10-9 演示了 ReturnType + Awaited 的套娃32。
Awaited:剥开 Promise 的那一层
Awaited<T> 做一件事:把 Promise<X> 的壳剥掉,留下 X。
原书给的类比很直白——它就是类型层面的 await33:
type AlbumPromise = Promise<Album>;
type AlbumResolved = Awaited<AlbumPromise>; // 悬停 → Album它最常出现的姿势是和 ReturnType 套娃:async 函数的 ReturnType 是
Promise<真正的值>,再套一层 Awaited 才是你要的:
type Data = Awaited<ReturnType<typeof fetchData>>;
// fetchData 返回 Promise<number> → Data 是 number这个「ReturnType + Awaited 双剥」组合在 10-9 里完整演练过32。 记忆钩子:凡是「拿到手的类型包了一层 Promise」的时刻,就轮到 Awaited—— 和第 05 章「async 函数返回 Promise」的规则互为表里:那章教你函数为什么会包壳, 这章教你类型怎么剥壳。
8. 变换并集:Exclude、Extract、NonNullable
最后三个工具处理并集过滤:
type AlbumState =
| { type: "released"; releaseDate: string }
| { type: "recording"; studio: string }
| { type: "mixing"; engineer: string };
// 排除:不要 released
type UnreleasedState = Exclude<AlbumState, { type: "released" }>;
// { type: "recording"; studio: string } | { type: "mixing"; engineer: string }
// 提取:只要 recording
type RecordingState = Extract<AlbumState, { type: "recording" }>;
// { type: "recording"; studio: string }工作方式类似模式匹配:Exclude 删掉「你给的模式能匹配的」成员(即模式的子类型),
Extract 留下匹配的成员。模式可以按判别子写,也可以按结构写
(Exclude<..., { releaseDate: string }> 同样能删掉 released——它有这个属性);
对原始类型并集也好用:Exclude<"a" | "b" | 1 | 2, string> 得 1 | 234。
NonNullable 是 Exclude 的特化:NonNullable<T> = Exclude<T, null | undefined>,
把空值从并集里清掉。典型场景:取可选属性的类型时顺手清理——
Album["artist"] 对 artist?: {...} 是 {...} | undefined,
NonNullable<Album["artist"]> 拿到干净的对象类型。写法更直白,所以优先用它35。
NonNullable 在真实代码里的出现位置,几乎总是「索引访问的下游」:
可选属性、可能为空的字典项、Promise<T | null> 的兑现值——
凡是「拿到一个可能带着 null/undefined 的类型,要交给下一个工具继续处理」的地方,
它都是进门前的地垫。记住它和 Exclude 的换算关系还有一个实用好处:
读别人的代码遇到 Exclude<T, null | undefined>,知道它就是 NonNullable,
不需要停下来查文档。
四工具对照表(原书 Table 10-1)及常见误用
| 工具 | 用在 | 动作 | 例子 |
|---|---|---|---|
| Exclude | 并集 | 删掉成员 | Exclude<"a" | 1, string> → 1 |
| Extract | 并集 | 留下成员 | Extract<"a" | 1, string> → "a" |
| Omit | 对象 | 删掉属性 | Omit<UserObj, "id"> |
| Pick | 对象 | 留下属性 | Pick<UserObj, "id"> |
最常见的误用:拿 Pick 对并集、拿 Exclude 对对象。 两个家族的分界是 「操作数是不是并集」——并集用 Exclude/Extract,对象用 Omit/Pick; 搞混了不会立刻报错,只是结果不是你想的(回想第 07 章 Omit 对联合不分配的坑)36。
推导工具速查(本章的口袋卡片)
| 需求 | 写法 | 结果 |
|---|---|---|
| 对象的键收成并集 | keyof Obj | "a" | "b" |
| 值的类型提出来 | typeof value | 值的推断类型 |
| 键并集(从值一步到位) | keyof typeof value | 键的字面量并集 |
| 某个属性的类型 | Obj["a"] | 属性类型 |
| 某几个属性的类型 | Obj["a" | "b"] | 属性类型的并集 |
| 对象全部值的并集 | Obj[keyof Obj] | 值类型并集 |
| 数组全部元素的并集 | Arr[number] | 元素类型并集(不混入方法) |
| 函数的参数元组 | Parameters<typeof fn> | [A, B, C] |
| 函数的返回类型 | ReturnType<typeof fn> | 返回类型 |
| 异步返回的兑现类型 | Awaited<ReturnType<typeof fn>> | 剥掉 Promise 后的类型 |
| 并集删成员 | Exclude<U, Pattern> | 剩下的成员 |
| 并集留成员 | Extract<U, Pattern> | 命中的成员 |
| 清掉 null/undefined | NonNullable<T> | 干净的 T |
读法全部遵循一条语法主线:typeof 负责「值→类型」的转换,其余工具都工作在类型世界。
所以任何以值开头的推导,第一招几乎总是 typeof(或 keyof typeof 的组合)。
记不住具体写法时,先问自己「我的源是值还是类型」——是值,先 typeof;是类型,
直接 keyof / 索引访问 / 工具类型。
这张速查表也是本章的「考试小抄」形态:十二行,每行一个需求一个写法。 建议的工作流是——真实代码里遇到「我想要某个类型」的时刻,先回这张表找行, 找到就把写法抄进去,悬停验证结果,再决定要不要起个名字存下来。 三次循环之后,最常用的六行(键并集、索引访问、T[number]、keyof typeof、 Parameters、ReturnType)会变成手写速度;剩下的六行留痕即可,用得到时再查。 工具的学习曲线从来不是背全,是用熟一半、认得另一半。
这句话也值得落在团队层面:推导工具的一致性,是整个代码库「类型方言」的 重要组成部分。同一类需求(比如「取配置对象的键」),有人写 keyof typeof、 有人手抄字面量并集,代码库的类型风格就裂成了两种方言——读者的查询成本翻倍, 重构时的爆炸半径也翻倍。给团队约定一份「推导工具首选表」(本章的速查卡就够), 是对本章学习成果最便宜的一次制度化。
9. 推导 vs 解耦:本章的清醒剂
工具齐了,原书立刻泼了冷水:推导是一种耦合。
「A 从 B 推导」意味着 A 依赖 B 的形状,甚至依赖 B 的存在——B 改了、挪了、拆了, A 全都得跟着想一遍。链式推导(A 从 B、B 从 C)会把耦合拉成一条链。 所以本章最后一块是决策问题:什么时候该推导,什么时候宁可手抄(解耦)?
该解耦的场景:关注点不同
原书的例子:React 组件 AvatarImage 渲染用户头像,只需要 User 的
imageUrl 和 name 两个属性。第一冲动是推导:
type AvatarImageProps = Pick<User, "imageUrl" | "name">;干净、高级、用了刚学的工具。但原书说:这里该解耦——手抄一份只有两个属性的类型:
type AvatarImageProps = {
imageUrl: string;
name: string;
};理由:User(数据层)和 AvatarImage(UI 层)是不同关注点,各有各的变更理由。
解耦之后,User 搬家、拆分、甚至删除,组件毫发无损;组件想加个 size 属性,
也不用去看数据层的脸色37。
原书对这段的心理剖析非常诚实,值得原样记住:
「推导会让你觉得自己很聪明。Pick 诱惑你,因为它用了 TypeScript 更高级的特性, 让你为自己的知识感到良好。但往往,做简单的事情、让类型保持解耦,才是更聪明的。」38
「用了高级特性所以用了」不是工程理由——这是全书对「类型体操冲动」最直接的一次警告。
该推导的场景:关注点相同
反过来,两个类型如果本来就同生共变,推导就是不二选择:
AlbumType从albumTypes推导:值表和它的值并集是同一个关注点, 解耦等于维护两份必同步的清单——busywork39;UserWithoutId = Omit<User, "id">从 User 推导:「没有 id 的 User」 的语义就是 User 的一部分,任何 User 的变更都应该流到它40。
把第二个例子走完整,你会看清「该推导」的形状:
type User = {
id: string;
name: string;
imageUrl: string;
email: string;
};
type UserWithoutId = Omit<User, "id">;
const updateUser = (id: string, user: UserWithoutId) => {
// 更新接口:主键走第一个参数,数据走第二个
};假如这里手抄一个解耦版:type UserWithoutId = { name: string; imageUrl: string; email: string }。
第一天它是对的;三个月后 User 加了 phone 字段——推导版自动要求调用方补 phone,
手抄版安静地漏掉它,直到某个调用方以为自己传了 phone 而实际类型里没有。
这就是「同步的重复」的代价形态:不报错,漂移。40
决策规则(原书原文的两问)
两个类型的关系,问两个问题:
① 一个的更新,是否需要传导到另一个? —— 是 → 推导
② 它们耦合起来,是否反而制造未来的工作量? —— 是 → 解耦
图说:不是「重复就要消除」,而是「同步的重复才该消除」。
独立演化的两份重复,是健康的隔离带。「推导 vs 解耦的决定,全在于减少未来的工作量」——这是原书给整个问题的一行总结41。
把两问决策套进两个对照场景,规则的用法就具体了:
场景 A(该推导):API 响应的字段名,和前端表单字段名
问 ①:后端改了字段名,前端要不要跟着改?——要(同步演进)
问 ②:耦合会制造未来工作量吗?——不会,改动本来就该同时发生
结论:推导(表单类型 = 响应类型的 Pick)
场景 B(该解耦):API 响应的字段名,和数据库列名
问 ①:后端改了响应字段,数据库列要不要跟着改?——不要(各自演化)
问 ②:耦合会制造未来工作量吗?——会,一次无害的重构拖出两处修改
结论:解耦(中间的映射层显式写两遍)
图说:同样的「重复」,一个该消、一个该留——区别不在重复本身,
在两份东西「是否注定同生共变」。这两问把「DRY 教条」变成了可操作的检查表。这个检查表最常见的误用是两问只问第一问:「要传导 → 推导」的推论漏了第二问的 刹车,结果把「偶尔会同步」的类型也耦合了,日后每次单边演进都拖出无妄的报错。 两问缺一不可,第二问是防止「推导狂热」的安全阀——这正是它排在第二的原因: 先给你推导出头的理由,再泼一盆冷水。
10. 八道练习:推导的实战手册
原书本章的练习是全书的「推导驾照考试」:八道題从 keyof 一路考到 ReturnType, 每道一个真实场景。走一遍,每道只取它的独家教训。
10-1:键重复的第一刀(keyof 的入门用法)
一个表单系统:FormValues 接口定义了提交字段(name/email/password),
inputs 记录又把这些键手写了一遍作为 Record 的键——同一份键清单,两处维护36。
const inputs: Record<
"name" | "email" | "password", // ← 手写的,和 FormValues 重复
{ initialValue: string; label: string }
> = { /* ... */ };解法一行:Record<keyof FormValues, ...>。键从接口推导而来,
FormValues 加一个字段,inputs 的键自动跟上;删一个字段,inputs 里多出来的键当场报错36。
这道题是「单一事实源」的最小完整样本:事实(键清单)只存在 FormValues 一处,
inputs 是它的投影。全书反复强调的 DRY,在类型层面的第一次落地就是这个形状。
10-2:typeof + keyof 的组合拳(从值反推联合)
三个部署环境(development/production/staging)的配置对象 configurations;
一个 Environment 类型要在全应用使用,但事实源是 configurations 对象37。
type Environment = keyof typeof configurations;
// 悬停 → "development" | "production" | "staging"两种写法等价:一行版(keyof typeof 连用),或先 type Configurations = typeof configurations
再 keyof Configurations——后者多一个可以悬停的中间名,调试时更友好37。
注意这里 typeof 的方向和第 04 章的运行时 typeof 完全相反:运行时那个问
「值的种类」,类型层面这个问「值的类型」——同一个词,两个世界(第 13 章的专题)。
从这道题起,keyof typeof 某对象 应该成为你的肌肉记忆:它是「从常量对象
生成联合类型」的标准起手式。
10-3:索引访问取单个值(点号走到类型里)
programModeEnumMap(as const 的常量对象)+ ProgramModeMap = typeof programModeEnumMap。
要求一个 Group 类型,永远和 map 里 GROUP 键的值同步38:
type Group = ProgramModeMap["GROUP"]; // 悬停 → "group"这正是第 5 节「索引访问 = 类型层面的点号」的实战:点号在值世界选属性,
方括号在类型世界选成员,语义完全同构38。和手写 type Group = "group" 相比,
推导版在 map 的值改时自动跟随——手写版则是又一份会腐烂的副本。
10-4:索引访问取并集(方括号里放联合)
同一张 map,这次要的是「所有 planned 开头的值」的并集。 关键一步:索引访问的方括号可以放进一个键的联合39:
type Key = "PLANNED_ONE_ON_ONE" | "PLANNED_SELF_DIRECTED";
type PlannedPrograms = ProgramModeMap[Key];
// 悬停 → "planned1on1" | "plannedSelfDirected"方括号里的联合会被逐个求值再合并结果——这是索引访问的分配行为, 和第 07 章映射类型对联合的分配是同一族特性。这道题的真正收获是意识到: 索引访问的返回值不一定是单个类型,按你喂的键的数量,可以是任意并集。
10-5:键并集 + 索引访问 = 全值并集(本章的「大招」)
还是同一张 map,这次要所有值的并集。解法把前两个工具合流:
type AllPrograms = (typeof programModeEnumMap)[keyof typeof programModeEnumMap];
// 悬停 → "group" | "announcement" | "1on1" | "selfDirected" | "planned1on1" | "plannedSelfDirected"拆开读:keyof typeof 生成键的并集,方括号把它喂给索引访问,
每个键取一个值,合并成值的并集40。也可以先存中间类型
(ProgramModeMap[keyof ProgramModeMap])——长表达式分段,悬停调试更容易。
这个双段式(keyof typeof + 索引访问)是「从常量映射导出值联合」的标准配方,
值得整段背下来——as const 枚举(第 6 节)、事件名表、路由表,全是它的应用场景。
10-6:as const 数组的陷阱与 number
任务和 10-5 同款,但源头从对象换成了 as const 数组。直觉解法照搬双段式, 结果炸出意想不到的成员:
type AllPrograms = (typeof programModes)[keyof typeof programModes];
// 悬停 → "group" | ... | 6 | (() => IterableIterator<...>) | ... 共 29 项!数组的 keyof 不是下标! 它包括 length、push、迭代器方法等所有数组成员—— 所以键的并集喂进去,取出来的除了六个字符串,还有数字 6 和一堆函数签名41。 (我们的拆解在 §5 已经完整解剖过这道题,这里从练习角度补一句: 它是全书最好的「照搬公式翻车」案例——公式的每个零件都合法,组合起来却错了, 因为没问 keyof 作用在这个类型上到底取到什么。)
正解一行:
type AllPrograms = (typeof programModes)[number];[number] 的含义是「所有可能下标的并集」——数组类型世界里,
number 就是合法的索引签名键(第 07 章索引签名的回响)41。
数组加新成员,联合自动扩展,零手工维护。T[number] 和
Obj[keyof Obj] 是一对孪生配方:一个服务数组,一个服务对象。
10-7:Parameters + typeof(从函数拿参数元组)
一个 makeQuery(url, opts?) 函数;想把它的参数表变成一个可引用的元组类型。
手写元组意味着第二份事实源:函数签名改了,元组不会自动跟42。
第一个坑立刻出现:Parameters<makeQuery> 报错——makeQuery 是值不是类型。
报错信息给了药方(Did you mean 'typeof makeQuery'?),照做:
type MakeQueryParameters = Parameters<typeof makeQuery>;
// 悬停 → [url: string, opts?: { method?: ...; headers?: ...; body?: ... } | undefined]
type Opts = MakeQueryParameters[1]; // 顺手取第二个参数的类型Parameters<typeof fn> 是「函数参数类型」的推导入口;取单个参数,
就再接一次索引访问([0]、[1])42。和第 5 节的教训合流:
typeof 是从值世界进类型世界的门票——值世界的任何名字(对象、函数),
进类型世界的第一站几乎都是 typeof。
10-8:ReturnType + typeof(从函数拿返回)
别的团队控制的 createUser 函数,实现看不到。想要一个和它的返回值
永远同步的 User 类型43:
type User = ReturnType<typeof createUser>;
// 悬停 → { id: string; name: string; email: string }和第 05 章「返回值可以推断」的规则合流:函数的返回值本来就是推断出来的, ReturnType 只是把这个推断结果取了个名字。对方团队改了返回形状, User 自动跟随——这对「跨团队消费函数」的场景是无价的安全带43。
八道题合成一张推导决策树
起点是? 目标是什么?
│
├─ 接口/类型的键 → keyof T
├─ 常量对象的键 → keyof typeof v
├─ 常量对象的某个值 → (typeof v)["键"]
├─ 常量对象的全部值 → (typeof v)[keyof typeof v]
├─ as const 数组的值 → (typeof v)[number]
├─ 函数的参数表 → Parameters<typeof fn>(再 [N] 取单个)
└─ 函数的返回值 → ReturnType<typeof fn>
图说:七个入口,七条固定路线。记不住细节时,回到这张图找路线,
到原文里对例子——八道题就是八份标准答案(见本节各脚注)。八道练习合起来,其实是同一种能力考了八次:「看着需求,选对搬运路线」。 差别只在于源的形态(对象、数组、函数)和目标的形态(键、值、参数、返回)。 所以这份手册的正确用法不是背八道题,而是把第 10 节末的决策树用到你自己的 代码里——第一次用推导工具解决自己的真实重复时,本章才算真正读完。
如果给这八次考试排个「最值回票价」的序,我们的答案是:10-6(数组的 [number])
10-5(全值并集)> 10-7(Parameters 挖参数)——前两个防的是最高频的 「键清单手写漂移」,第三个打开的是「第三方库挖类型」这扇大门。 其余五道是这三道的变奏,时间紧时先读这三道也能拿到本章八成的收益。 第十一章的拆解到这里完整——钥匙都交给你了,下一章处理「钥匙开不了的锁」。
11. 这一章在全书链条中的位置
推导是把第 07 章工具类型「内化」的一步:上一章你使用 Pick/Omit,
这一章你学会了生成它们的参数(keyof 出键清单)和绕过它们的边界
(typeof 从函数签名里挖类型)。而「推导是引用、引用即耦合」这条因果,
是通向第 16 章(条件类型、映射类型)的桥:那里你会亲手写出 Exclude 和
DistributiveOmit 的实现——本章的每一个工具,底层都是第 16 章的两个机制
(条件类型 + 索引访问)。原书小结把这章收在推导/解耦的决策上,
而不是工具清单上——工具会过时,决策规则不会42。
把这个位置关系画成一张全书的小地图,本章的坐标就更清楚了:
第 04–06 章:类型的基本词汇(并集、收窄、形状)
第 07 章: 借来的工具(Pick/Omit/Record——别人的推导成果)
第 08 章: 推断的原料产地(as const 的字面量精度)
本章: 推导的语法(typeof/keyof/索引访问)与伦理(推导 vs 解耦)
第 12 章: 推导的例外处置(satisfies——推导不够时的补丁)
第 16 章: 推导的引擎室(亲手写工具的实现)
图说:本章是全书的「腰」——往前是消费,往后是生产。
读懂这一章,后面两章的所有新工具都只是「这里语法的组合应用」。这也是本章在全书的真正意义:它不是工具最多的一章(第 07 章才是), 但它是从「使用类型的人」变成「制造类型的人」的转正考核—— 过了这关,第 16 章的泛型、映射、条件类型就只是新语法的组装, 而不是新心智模型的负担。作者把 Monaco 案例放在这里,正是给「转正者」的第一个 真实任务:库里缺的类型,自己去挖。
顺着这张地图,还能看清本章和第 12 章的分工:第 12 章解决「推导够不着」的角落 (值既要受检查、推断又要保留——satisfies 登场),是本章的补丁章; 两章合起来构成「单一事实源」的完整工具箱——推导负责「够得着就搬」, satisfies 负责「够不着就补」。先学推导再学补丁的顺序也是刻意的: 知道工具能做什么,才知道哪里是补丁的用武之地。
12. 作者的判断与证据(分开标)
书里有据的:
- keyof 跟随源更新、typeof 的位置双义、索引访问的方括号语法——语言机制, 全部配了悬停实证4313。
- 数组的
[keyof]会混入方法类型——IterableIterator 的悬停结果在案,是本章最生动的「坑的解剖」18。 - Monaco Editor 案例——第一人称的真实库案例,三步推导链完整可查30。
- Exclude/Extract 的模式匹配语义、NonNullable = Exclude 的特化——机制描述加对照实例3435。
- 推导 vs 解耦的两问决策规则——原书明确写成「The decision ... is all about reducing your future workload」41;Pick 的「聪明陷阱」自省是价值判断,不是技术论断38。
原书没说的(我们补):
Parameters/ReturnType对重载函数只取最后一个签名——补充(来自通用知识): 重载的完整处理需要手写条件类型(第 16 章之后才有工具讨论)。- 索引访问
Obj[K]对K的类型安全(keyof 约束)在泛型中的形式——K extends keyof Obj在第 16 章才出现;本章的索引访问都是「具体键」。 - Exclude 的模式匹配基于「可分配性」(assignability),与结构化子类型的关系——
补充(来自通用知识):
Exclude<A, B>删掉的是「可以赋给 B 的 A 的成员」, 这正是第 06 章宽窄规则的复用。
判断(我们的,不是书里的): 「推导 vs 解耦」这一节是全书的隐藏高点—— 它把一个工具教材升级成了工程判断教材。大多数类型教程止步于「教你更多工具」, 这一节反过来教你「什么时候不用工具」。两问决策(更新要不要传导?耦合会不会造工作量?) 的本质是:类型的 DRY 不追求「不重复」,追求「重复不漂移」—— 两份注定要一起改的重复是 bug 温床,两份各自演化的重复是隔离带。 这个判据放到函数、组件、微服务上同样成立,它已经超出类型系统的边界了。 如果错,会错在: 在小型项目或原型里,「关注点是否相同」的分辨成本可能高于 耦合的代价——一律推导反而更快;两问规则是中型以上项目的守则,不是宇宙真理。
判断(我们的,不是书里的): 本章工具的隐藏成本是「推导链的断点排查」。 推导链断裂时(上游改了、下游报错),报错位置在下游的推导式, 而不是上游的改动点——你得到的是「AllPrograms 不再匹配」, 而不是「programModeEnumMap 删了一个键」。链越长,报错离根越远, 排查越像沿着河流找污染源。对策是把推导链分段并命名: 每个中间站起一个类型名(ProgramModeMap、ProgramModeKey), 报错就停在最近的站,而不是链尾。这条「分段命名」纪律, 和写长表达式时拆中间变量的道理完全一样——只是类型表达式更容易被 写成一整行「炫技式推导」,因为类型没有运行时性能代价。 如果错,会错在: 短链(两站内)的报错定位其实足够直接, 分段命名反而增加名字管理的负担;纪律的适用范围是「链长 ≥ 3 站」的场景, 这个阈值是我们定的经验值,不是原书的规矩。
判断(我们的,不是书里的): 原书把 Monaco 案例(从第三方库挖类型) 放在函数推导一节,是本章「务实程度」的峰值——它展示的不是语法,是姿态: 编译器知道的比你多,推导工具是让你把编译器的知识取出来用的合法手段。 这个姿态的推论很实际:遇到「库里没导出这个类型」时,第一反应不该是
any或复制粘贴,而是「它的签名里一定有,我去取」。全书工具的真正价值排序, 我们认为是:这种「取回被藏起来的类型」的能力 > 防重复 > 炫技式的类型编程。 如果错,会错在: 过度依赖挖类型可能养成「不读库文档」的习惯—— 挖出来的类型是行为的投影,不告诉你设计意图;意图仍然只在文档和人类经验里。 工具和文档的互补关系,和第 02 章「补全替代翻阅、不替代理解」是同一个结论。
13. 边界与局限
- 推导链的长度是负债:A 从 B、B 从 C、C 从 D——重构 D 时的爆炸半径要穿过整条链。 本章工具越强,越要克制链长;三层以上的推导链就该起中间名字。
- typeof 只能「读」不能「写」:它忠实反映值的推断类型,包括你不想要的拓宽
(忘了
as const时拿到string而不是字面量)——推导的精度上限,是源的推断精度。 - 工具类型的嵌套可读性:
Awaited<ReturnType<typeof fetchUser>>已接近可读性上限; 再深的套娃该拆中间类型(原书在 10-9 的解法后也这么说)32。 - Exclude/Extract 的模式匹配对「结构恰好是子集」的成员会误伤/误收——
判别子精确匹配(
{type: "released"})比结构匹配({releaseDate: string})更稳。 - 推导的另一个上限是「形状漂移」:推导反映的是当前的类型; 值的一侧如果 evolving(第 13 章)或 any,推导结果就是 any/unknown 的宽类型。 推导不会替你把松散的源头拧紧——它是忠实的镜子,不是美容仪。
keyof对数组/函数的结果常常不是直觉答案(10-6 的教训): 数组的 keyof 包括 length 和全部方法,函数的 keyof 是一串工具键。 对「类数组」结构求键之前,先悬停看一眼 keyof 的真实结果,别按对象的经验套。- 推导类型不能被「选择性放宽」:从常量推导出的联合是精确的; 想让某个成员宽一点,只能回到源头改常量,或套一层显式注解—— 推导的类型没有「近似版」,这既是它的力量也是它的僵硬。
14. 可带走的
- 单一事实源:类型从值/其他类型推导,源改一处、处处生效;手抄是债,推导是引用。
keyof Obj→ 键的并集;typeof value→ 值的类型;keyof typeof v→ 键并集(高频惯用语)。- 类型层面取属性用方括号:
Obj["a"];点号是运行时语法,类型里报错。 - 索引访问可以喂并集:
Obj["a" | "b"];Obj[keyof Obj]= 全部值的并集。 - 对象的全部值用
[keyof Obj],数组的全部元素用[number]——后者不会混入方法类型。 - 值 → 类型是单向门:没有 valueof,类型造不出值。
Parameters<typeof fn>得参数元组(可索引),ReturnType<typeof fn>得返回,Awaited<...>剥 Promise;挖第三方库未导出的类型,是它们的头号战场。Exclude删并集成员、Extract留并集成员(模式匹配);NonNullable清 null/undefined。- 并集用 Exclude/Extract,对象用 Omit/Pick——两家族别串门。
- as const 对象 + 三招推导 = enum 的现代替代:结构化、零魔法、团队习惯 enum 则 enum。
- 推导 = 耦合;同生共变的类型才推导,关注点不同就手抄——「聪明」不是工程理由。
- 推导的洋葱流程:Parameters 剥参数 → 索引访问剥属性 → Awaited 剥 Promise。
- 套娃超过两层就拆中间类型——可读性优先于简洁。
- 推导不创造精度,只搬运源的推断——结果意外变宽,先回头查源(多半忘了 as const)。
- NonNullable 是索引访问的下游地垫:拿到可能带 null/undefined 的类型,先垫一层再往下传。
第 15 条值得展开半句,因为它容易被误读成「NonNullable 到处垫」。 正确的用法是「一次垫、多处用」:在推导链的起点把 null/undefined 清掉, 下游拿到的是干净类型;处处垫则每个消费点都要思考「这里会不会有 null」, 思考成本反而高于收益。垫的位置 = 类型的边界,这是它和收窄(第 06 章) 的分工:收窄管代码流,NonNullable 管类型定义。
最后给这十五条一个总纲:本章的全部工具,都是「单一事实源」这四个字的不同写法。 keyof、typeof、索引访问、工具类型,形态各异,做的都是同一件事—— 让一份事实只写一次,其余位置成为它的投影。带着这四个字回看你的代码库, 哪些地方在「手抄事实」,哪些地方已经「投影化」,一目了然; 前者就是本章工具的用武之地,而后者值得作为风格基准推广到团队。
总纲之外还有一个反面的提醒,和第 9 节的清醒剂互为表里:单一事实源不是 「只写一处」的洁癖,而是「该同步的写一处」的判断。判断错了方向, 投影化反而把不该耦合的东西拴在了一起。所以本章真正的毕业标准不是 「能用十个推导工具」,而是「面对任何两份类型,能说出它们该不该同步」—— 工具是答案,判断才是能力。第十一章的推导,到这里拆完——十二章接着讲「推导之后的最后一公里」: 当推导把类型送到了正确的位置,注解和断言决定最后由谁说了算。两章之间的过渡, 正是全书从「怎么写对」走向「怎么沟通」的分水岭。顺便把本章的一句话版本留给赶时间的读者: typeof 进门,keyof 和方括号在屋里搬东西,两问决策决定哪些东西值得搬。 其余的一切——Exclude、Parameters、Awaited、as const 迁移——都是这句话的注脚。 读完全章再回来看这句,每个词都该有落点了;如果还有词没着落,那就是哪一节 需要再读一遍的路标。第十一章拆解完毕,全部新增内容均可按下方地图回核原文—— 从八道题到迁移路线,每一处的行号都经过机器校验。
15. 原文地图
本章地图覆盖原书第 10 章(Deriving Types from Other Types)的全部小节与八道练习。 新增条目对应加厚内容:八道练习的完整走查(第 10 节)、Monaco 挖类型案例的 洋葱流程、Awaited 的双剥组合、「没有 valueof」的单向门论证、as const 迁移路线、 keyof 的引用语义与数组边界——每一处都可以在转码文本里按行号 + 搜词两步回核。
| 主题 | 原书位置 | 原文位置 |
|---|---|---|
| 本章的定位(DRY 对类型) | Deriving Types from Other Types | text/26-fm-deriving-types-from-other-types.txt:6(搜「Keep code DRY」) |
| 派生关系是单向的 | 同上 | text/26-fm-deriving-types-from-other-types.txt:45(搜「one-way」) |
| keyof 的定义 | 同上 | text/26-fm-deriving-types-from-other-types.txt:55(搜「extract the keys」) |
| keyof 跟随源更新 | 同上 | text/26-fm-deriving-types-from-other-types.txt:68(搜「automatically be reflected」) |
| typeof 的定义 | 同上 | text/26-fm-deriving-types-from-other-types.txt:92(搜「extract a type from a value」) |
| typeof 的位置双义 | 同上 | text/26-fm-deriving-types-from-other-types.txt:127(搜「Runtime typeof」) |
| keyof typeof 组合 | 同上 | text/26-fm-deriving-types-from-other-types.txt:118(搜「keyof typeof albumSales」) |
| 没有 valueof(单向门) | 同上 | text/26-fm-deriving-types-from-other-types.txt:153(搜「the other way around」) |
| 点号报错与方括号正解 | 同上 | text/26-fm-deriving-types-from-other-types.txt:173(搜「Did you mean to retrieve」) |
| 索引访问喂并集 | 同上 | text/26-fm-deriving-types-from-other-types.txt:225(搜「AlbumPropertyTypes = Album」) |
| Obj[keyof Obj] 全部值 | 同上 | text/26-fm-deriving-types-from-other-types.txt:235(搜「union of all the values」) |
| as const 枚举三招 | 同上 | text/26-fm-deriving-types-from-other-types.txt:253(搜「keyof typeof albumTypes」) |
| POJO 路线的评价 | 同上 | text/26-fm-deriving-types-from-other-types.txt:263(搜「Plain Old JavaScript Object」) |
| 裸字符串放行的取舍 | 同上 | text/26-fm-deriving-types-from-other-types.txt:275(搜「make refactoring a little harder」) |
| 名目 vs 结构的取舍 | 同上 | text/26-fm-deriving-types-from-other-types.txt:317(搜「more restrictive」) |
| 「今天开项目用 as const」 | 同上 | text/26-fm-deriving-types-from-other-types.txt:327(搜「starting a project today」) |
| 数组 [keyof] 的污染 | 同上 | text/26-fm-deriving-types-from-other-types.txt:618(搜「IterableIterator」) |
| [number] 正解 | 同上 | text/26-fm-deriving-types-from-other-types.txt:630(搜「number type as the argument」) |
| Parameters | 同上 | text/26-fm-deriving-types-from-other-types.txt:641(搜「extracts the parameters」) |
| 必须先 typeof | 同上 | text/26-fm-deriving-types-from-other-types.txt:656(搜「won’t work on its own」) |
| Awaited | 同上 | text/26-fm-deriving-types-from-other-types.txt:680(搜「unwrap the Promise」) |
| Monaco Editor 三步推导 | 同上 | text/26-fm-deriving-types-from-other-types.txt:728(搜「EditorProps = Parameters」) |
| Exclude 的模式匹配 | 同上 | text/26-fm-deriving-types-from-other-types.txt:951(搜「pattern matching」) |
| NonNullable = Exclude 特化 | 同上 | text/26-fm-deriving-types-from-other-types.txt:977(搜「operates similarly」) |
| 四工具对照表 | 同上 | text/26-fm-deriving-types-from-other-types.txt:1007(搜「Table 10-1」) |
| 推导是耦合 | 同上 | text/26-fm-deriving-types-from-other-types.txt:49(搜「a form of coupling」) |
| AvatarImage 该解耦 | 同上 | text/26-fm-deriving-types-from-other-types.txt:1091(搜「unaffected」) |
| 「聪明陷阱」自省 | 同上 | text/26-fm-deriving-types-from-other-types.txt:1095(搜「feel clever」) |
| 两问决策规则 | 同上 | text/26-fm-deriving-types-from-other-types.txt:1133(搜「need to ripple」) |
| 本章小结 | 同上 | text/26-fm-deriving-types-from-other-types.txt:1139(搜「single sources of truth」) |
| 练习 10-1:键重复的第一刀 | Deriving Types from Other Types | text/26-fm-deriving-types-from-other-types.txt:373(搜「keyof FormValues」) |
| 练习 10-2:typeof + keyof | 同上 | text/26-fm-deriving-types-from-other-types.txt:419(搜「keyof typeof configurations」) |
| 练习 10-3:索引访问取单值 | 同上 | text/26-fm-deriving-types-from-other-types.txt:472(搜「ProgramModeMap["GROUP"]」) |
| 练习 10-4:索引访问取并集 | 同上 | text/26-fm-deriving-types-from-other-types.txt:507(搜「PLANNED_ONE_ON_ONE」) |
| 练习 10-5:全值并集 | 同上 | text/26-fm-deriving-types-from-other-types.txt:553(搜「[keyof typeof programModeEnumMap]」) |
| 练习 10-6:as const 数组与 [number] | 同上 | text/26-fm-deriving-types-from-other-types.txt:605(搜「IterableIterator」) · text/26-fm-deriving-types-from-other-types.txt:630(搜「all possible index values」) |
| 练习 10-7:Parameters + typeof | 同上 | text/26-fm-deriving-types-from-other-types.txt:789(搜「Parameters<typeof makeQuery>」) |
| 练习 10-8:ReturnType + typeof | 同上 | text/26-fm-deriving-types-from-other-types.txt:849(搜「ReturnType<typeof createUser>」) |
| Monaco 挖类型案例 | 同上 | text/26-fm-deriving-types-from-other-types.txt:728(搜「Parameters<typeof Editor>」) |
| Awaited 的类比 | 同上 | text/26-fm-deriving-types-from-other-types.txt:680(搜「similar to using the await」) |
| 「没有 valueof」 | 同上 | text/26-fm-deriving-types-from-other-types.txt:138(搜「no way to create a value」) |
Footnotes
-
出处:「Deriving Types from Other Types」第 444–462 段(
text/26-fm-deriving-types-from-other-types.txt:454,搜「PLANNED_SELF_DIRECTED」)。 ↩ -
出处:「Deriving Types from Other Types」第 90–134 段(
text/26-fm-deriving-types-from-other-types.txt:92,搜「extract a type from a value」)。 ↩ -
出处:「Deriving Types from Other Types」第 125–132 段(
text/26-fm-deriving-types-from-other-types.txt:127,搜「Runtime typeof」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 53–88 段(
text/26-fm-deriving-types-from-other-types.txt:55,搜「extract the keys」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 113–118 段(
text/26-fm-deriving-types-from-other-types.txt:118,搜「keyof typeof albumSales」)。 ↩ -
出处:「Deriving Types from Other Types」第 230–235 段(
text/26-fm-deriving-types-from-other-types.txt:235,搜「union of all the values」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 68–77 段(
text/26-fm-deriving-types-from-other-types.txt:68,搜「automatically be reflected」)。 ↩ -
出处:「Deriving Types from Other Types」第 79–88 段(
text/26-fm-deriving-types-from-other-types.txt:87,搜「producer」)。 ↩ -
出处:「Deriving Types from Other Types」第 329–381 段(
text/26-fm-deriving-types-from-other-types.txt:373,搜「keyof FormValues」)。 ↩ -
出处:「Deriving Types from Other Types」第 134 段(
text/26-fm-deriving-types-from-other-types.txt:134,搜「objects, functions, classes」)。 ↩ -
出处:「Deriving Types from Other Types」第 136–153 段(
text/26-fm-deriving-types-from-other-types.txt:153,搜「the other way around」)。 ↩ -
出处:「Deriving Types from Other Types」第 383–442 段(
text/26-fm-deriving-types-from-other-types.txt:419,搜「keyof typeof configurations」)。 ↩ -
出处:「Deriving Types from Other Types」第 155–182 段(
text/26-fm-deriving-types-from-other-types.txt:173,搜「Did you mean to retrieve」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 184–189 段(
text/26-fm-deriving-types-from-other-types.txt:187,搜「AlbumTuple[0]」)。 ↩ -
出处:「Deriving Types from Other Types」第 191–206 段(
text/26-fm-deriving-types-from-other-types.txt:204,搜「ArtistName」)。 ↩ -
出处:「Deriving Types from Other Types」第 208–228 段(
text/26-fm-deriving-types-from-other-types.txt:225,搜「AlbumPropertyTypes = Album」)。 ↩ -
出处:「Deriving Types from Other Types」第 444–568 段(
text/26-fm-deriving-types-from-other-types.txt:472,搜「Group = ProgramModeMap」)、第 507–510 段(搜「Key =」)、第 553 段(搜「keyof typeof programModeEnumMap」)。 ↩ -
出处:「Deriving Types from Other Types」第 570–628 段(
text/26-fm-deriving-types-from-other-types.txt:618,搜「IterableIterator」)。 ↩ ↩2 ↩3 -
出处:「Deriving Types from Other Types」第 630–633 段(
text/26-fm-deriving-types-from-other-types.txt:630,搜「number type as the argument」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 633 段(
text/26-fm-deriving-types-from-other-types.txt:633,搜「great pattern」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 237–263 段(
text/26-fm-deriving-types-from-other-types.txt:263,搜「Plain Old JavaScript Object」)。 ↩ -
出处:「Deriving Types from Other Types」第 267–275 段(
text/26-fm-deriving-types-from-other-types.txt:275,搜「make refactoring a little harder」)。 ↩ -
出处:「Deriving Types from Other Types」第 277–317 段(
text/26-fm-deriving-types-from-other-types.txt:300,搜「doesn’t matter」)。 ↩ -
出处:「Deriving Types from Other Types」第 239–263 段(
text/26-fm-deriving-types-from-other-types.txt:249,搜「as const」)。 ↩ -
出处:「Deriving Types from Other Types」第 319–327 段(
text/26-fm-deriving-types-from-other-types.txt:327,搜「starting a project today」)。 ↩ -
出处:「Deriving Types from Other Types」第 325 段(
text/26-fm-deriving-types-from-other-types.txt:325,搜「in-progress proposals」)。 ↩ -
出处:「Deriving Types from Other Types」第 639–663 段(
text/26-fm-deriving-types-from-other-types.txt:656,搜「won’t work on its own」)。 ↩ -
出处:「Deriving Types from Other Types」第 661–663 段(
text/26-fm-deriving-types-from-other-types.txt:663,搜「SellAlbumParams[1]」)。 ↩ -
出处:「Deriving Types from Other Types」第 676–686 段(
text/26-fm-deriving-types-from-other-types.txt:680,搜「unwrap the Promise」)。 ↩ -
出处:「Deriving Types from Other Types」第 688–737 段(
text/26-fm-deriving-types-from-other-types.txt:728,搜「EditorProps = Parameters」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 739–811 段(
text/26-fm-deriving-types-from-other-types.txt:789,搜「Parameters<typeof makeQuery>」)。 ↩ -
出处:「Deriving Types from Other Types」第 860–908 段(
text/26-fm-deriving-types-from-other-types.txt:908,搜「more succinct solution」)。 ↩ ↩2 ↩3 -
出处:「Deriving Types from Other Types」Awaited 一节(
text/26-fm-deriving-types-from-other-types.txt:680,搜「similar to using the await」)。 ↩ -
出处:「Deriving Types from Other Types」第 914–959 段(
text/26-fm-deriving-types-from-other-types.txt:951,搜「pattern matching」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 961–980 段(
text/26-fm-deriving-types-from-other-types.txt:977,搜「operates similarly」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 1005–1007 段(
text/26-fm-deriving-types-from-other-types.txt:1005,搜「common mistake」)。 ↩ ↩2 ↩3 -
出处:「Deriving Types from Other Types」第 1049–1095 段(
text/26-fm-deriving-types-from-other-types.txt:1091,搜「unaffected」)。 ↩ ↩2 ↩3 -
出处:「Deriving Types from Other Types」第 1095 段(
text/26-fm-deriving-types-from-other-types.txt:1095,搜「feel clever」)。 ↩ ↩2 ↩3 ↩4 -
出处:「Deriving Types from Other Types」第 1097–1112 段(
text/26-fm-deriving-types-from-other-types.txt:1112,搜「closely related」)。 ↩ ↩2 -
出处:「Deriving Types from Other Types」第 1114–1129 段(
text/26-fm-deriving-types-from-other-types.txt:1123,搜「UserWithoutId」)。 ↩ ↩2 ↩3 -
出处:「Deriving Types from Other Types」第 1131–1135 段(
text/26-fm-deriving-types-from-other-types.txt:1133,搜「need to ripple」)。 ↩ ↩2 ↩3 ↩4 -
出处:「Deriving Types from Other Types」第 1137–1149 段(
text/26-fm-deriving-types-from-other-types.txt:1149,搜「Derive when」)。 ↩ ↩2 ↩3 -
出处:「Deriving Types from Other Types」练习 10-8 解答(
text/26-fm-deriving-types-from-other-types.txt:849,搜「ReturnType<typeof createUser>」)。 ↩ ↩2