跳转至

类型推导 — 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 | number

16 巧写法的再进一步: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。 原书给的类比很直白——它就是类型层面的 await43:

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 | 233。

NonNullable 是 Exclude 的特化:NonNullable<T> = Exclude<T, null | undefined>, 把空值从并集里清掉。典型场景:取可选属性的类型时顺手清理—— Album["artist"] 对 artist?: {...} 是 {...} | undefined, NonNullable<Album["artist"]> 拿到干净的对象类型。写法更直白,所以优先用它34。

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 对联合不分配的坑)35。

推导工具速查(本章的口袋卡片)

需求 写法 结果
对象的键收成并集 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 属性, 也不用去看数据层的脸色36。

原书对这段的心理剖析非常诚实,值得原样记住:

「推导会让你觉得自己很聪明。Pick 诱惑你,因为它用了 TypeScript 更高级的特性, 让你为自己的知识感到良好。但往往,做简单的事情、让类型保持解耦,才是更聪明的。」37

「用了高级特性所以用了」不是工程理由——这是全书对「类型体操冲动」最直接的一次警告。

该推导的场景:关注点相同

反过来,两个类型如果本来就同生共变,推导就是不二选择:

  • AlbumType 从 albumTypes 推导:值表和它的值并集是同一个关注点, 解耦等于维护两份必同步的清单——busywork38;
  • UserWithoutId = Omit<User, "id"> 从 User 推导:「没有 id 的 User」 的语义就是 User 的一部分,任何 User 的变更都应该流到它39。

把第二个例子走完整,你会看清「该推导」的形状:

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 而实际类型里没有。 这就是「同步的重复」的代价形态:不报错,漂移。39

决策规则(原书原文的两问)

  两个类型的关系,问两个问题:
  ① 一个的更新,是否需要传导到另一个?  —— 是 → 推导
  ② 它们耦合起来,是否反而制造未来的工作量? —— 是 → 解耦

  图说:不是「重复就要消除」,而是「同步的重复才该消除」。
  独立演化的两份重复,是健康的隔离带。

「推导 vs 解耦的决定,全在于减少未来的工作量」——这是原书给整个问题的一行总结40。

把两问决策套进两个对照场景,规则的用法就具体了:

  场景 A(该推导):API 响应的字段名,和前端表单字段名
    问 ①:后端改了字段名,前端要不要跟着改?——要(同步演进)
    问 ②:耦合会制造未来工作量吗?——不会,改动本来就该同时发生
    结论:推导(表单类型 = 响应类型的 Pick)

  场景 B(该解耦):API 响应的字段名,和数据库列名
    问 ①:后端改了响应字段,数据库列要不要跟着改?——不要(各自演化)
    问 ②:耦合会制造未来工作量吗?——会,一次无害的重构拖出两处修改
    结论:解耦(中间的映射层显式写两遍)

  图说:同样的「重复」,一个该消、一个该留——区别不在重复本身,
  在两份东西「是否注定同生共变」。这两问把「DRY 教条」变成了可操作的检查表。

这个检查表最常见的误用是两问只问第一问:「要传导 → 推导」的推论漏了第二问的 刹车,结果把「偶尔会同步」的类型也耦合了,日后每次单边演进都拖出无妄的报错。 两问缺一不可,第二问是防止「推导狂热」的安全阀——这正是它排在第二的原因: 先给你推导出头的理由,再泼一盆冷水。

10. 八道练习:推导的实战手册

原书本章的练习是全书的「推导驾照考试」:八道題从 keyof 一路考到 ReturnType, 每道一个真实场景。走一遍,每道只取它的独家教训。

10-1:键重复的第一刀(keyof 的入门用法)

一个表单系统:FormValues 接口定义了提交字段(name/email/password), inputs 记录又把这些键手写了一遍作为 Record 的键——同一份键清单,两处维护35。

const inputs: Record<
  "name" | "email" | "password",  // ← 手写的,和 FormValues 重复
  { initialValue: string; label: string }
> = { /* ... */ };

解法一行:Record<keyof FormValues, ...>。键从接口推导而来, FormValues 加一个字段,inputs 的键自动跟上;删一个字段,inputs 里多出来的键当场报错35。 这道题是「单一事实源」的最小完整样本:事实(键清单)只存在 FormValues 一处, inputs 是它的投影。全书反复强调的 DRY,在类型层面的第一次落地就是这个形状。

10-2:typeof + keyof 的组合拳(从值反推联合)

三个部署环境(development/production/staging)的配置对象 configurations; 一个 Environment 类型要在全应用使用,但事实源是 configurations 对象36。

type Environment = keyof typeof configurations;
// 悬停 → "development" | "production" | "staging"

两种写法等价:一行版(keyof typeof 连用),或先 type Configurations = typeof configurations 再 keyof Configurations——后者多一个可以悬停的中间名,调试时更友好36。 注意这里 typeof 的方向和第 04 章的运行时 typeof 完全相反:运行时那个问 「值的种类」,类型层面这个问「值的类型」——同一个词,两个世界(第 13 章的专题)。 从这道题起,keyof typeof 某对象 应该成为你的肌肉记忆:它是「从常量对象 生成联合类型」的标准起手式。

10-3:索引访问取单个值(点号走到类型里)

programModeEnumMap(as const 的常量对象)+ ProgramModeMap = typeof programModeEnumMap。 要求一个 Group 类型,永远和 map 里 GROUP 键的值同步37:

type Group = ProgramModeMap["GROUP"];  // 悬停 → "group"

这正是第 5 节「索引访问 = 类型层面的点号」的实战:点号在值世界选属性, 方括号在类型世界选成员,语义完全同构37。和手写 type Group = "group" 相比, 推导版在 map 的值改时自动跟随——手写版则是又一份会腐烂的副本。

10-4:索引访问取并集(方括号里放联合)

同一张 map,这次要的是「所有 planned 开头的值」的并集。 关键一步:索引访问的方括号可以放进一个键的联合38:

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 生成键的并集,方括号把它喂给索引访问, 每个键取一个值,合并成值的并集39。也可以先存中间类型 (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 和一堆函数签名40。 (我们的拆解在 §5 已经完整解剖过这道题,这里从练习角度补一句: 它是全书最好的「照搬公式翻车」案例——公式的每个零件都合法,组合起来却错了, 因为没问 keyof 作用在这个类型上到底取到什么。)

正解一行:

type AllPrograms = (typeof programModes)[number];

[number] 的含义是「所有可能下标的并集」——数组类型世界里, number 就是合法的索引签名键(第 07 章索引签名的回响)40。 数组加新成员,联合自动扩展,零手工维护。T[number] 和 Obj[keyof Obj] 是一对孪生配方:一个服务数组,一个服务对象。

10-7:Parameters + typeof(从函数拿参数元组)

一个 makeQuery(url, opts?) 函数;想把它的参数表变成一个可引用的元组类型。 手写元组意味着第二份事实源:函数签名改了,元组不会自动跟41。

第一个坑立刻出现: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])41。和第 5 节的教训合流: typeof 是从值世界进类型世界的门票——值世界的任何名字(对象、函数), 进类型世界的第一站几乎都是 typeof。

10-8:ReturnType + typeof(从函数拿返回)

别的团队控制的 createUser 函数,实现看不到。想要一个和它的返回值 永远同步的 User 类型42:

type User = ReturnType<typeof createUser>;
// 悬停 → { id: string; name: string; email: string }

和第 05 章「返回值可以推断」的规则合流:函数的返回值本来就是推断出来的, ReturnType 只是把这个推断结果取了个名字。对方团队改了返回形状, User 自动跟随——这对「跨团队消费函数」的场景是无价的安全带42。

八道题合成一张推导决策树

  起点是?                目标是什么?
    │
    ├─ 接口/类型的键     → 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 章的两个机制 (条件类型 + 索引访问)。原书小结把这章收在推导/解耦的决策上, 而不是工具清单上——工具会过时,决策规则不会41。

把这个位置关系画成一张全书的小地图,本章的坐标就更清楚了:

  第 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 的特化——机制描述加对照实例3334。
  • 推导 vs 解耦的两问决策规则——原书明确写成「The decision ... is all about reducing your future workload」40;Pick 的「聪明陷阱」自省是价值判断,不是技术论断37。

原书没说的(我们补):

  • 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. 可带走的

  1. 单一事实源:类型从值/其他类型推导,源改一处、处处生效;手抄是债,推导是引用。
  2. keyof Obj → 键的并集;typeof value → 值的类型;keyof typeof v → 键并集(高频惯用语)。
  3. 类型层面取属性用方括号:Obj["a"];点号是运行时语法,类型里报错。
  4. 索引访问可以喂并集:Obj["a" | "b"];Obj[keyof Obj] = 全部值的并集。
  5. 对象的全部值用 [keyof Obj],数组的全部元素用 [number]——后者不会混入方法类型。
  6. 值 → 类型是单向门:没有 valueof,类型造不出值。
  7. Parameters<typeof fn> 得参数元组(可索引),ReturnType<typeof fn> 得返回, Awaited<...> 剥 Promise;挖第三方库未导出的类型,是它们的头号战场。
  8. Exclude 删并集成员、Extract 留并集成员(模式匹配);NonNullable 清 null/undefined。
  9. 并集用 Exclude/Extract,对象用 Omit/Pick——两家族别串门。
  10. as const 对象 + 三招推导 = enum 的现代替代:结构化、零魔法、团队习惯 enum 则 enum。
  11. 推导 = 耦合;同生共变的类型才推导,关注点不同就手抄——「聪明」不是工程理由。
  12. 推导的洋葱流程:Parameters 剥参数 → 索引访问剥属性 → Awaited 剥 Promise。
  13. 套娃超过两层就拆中间类型——可读性优先于简洁。
  14. 推导不创造精度,只搬运源的推断——结果意外变宽,先回头查源(多半忘了 as const)。
  15. 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」)


  1. 出处:「Deriving Types from Other Types」第 444–462 段(text/26-fm-deriving-types-from-other-types.txt:454,搜「PLANNED_SELF_DIRECTED」)。 ↩

  2. 出处:「Deriving Types from Other Types」第 90–134 段(text/26-fm-deriving-types-from-other-types.txt:92,搜「extract a type from a value」)。 ↩

  3. 出处:「Deriving Types from Other Types」第 125–132 段(text/26-fm-deriving-types-from-other-types.txt:127,搜「Runtime typeof」)。 ↩↩

  4. 出处:「Deriving Types from Other Types」第 53–88 段(text/26-fm-deriving-types-from-other-types.txt:55,搜「extract the keys」)。 ↩↩

  5. 出处:「Deriving Types from Other Types」第 113–118 段(text/26-fm-deriving-types-from-other-types.txt:118,搜「keyof typeof albumSales」)。 ↩

  6. 出处:「Deriving Types from Other Types」第 230–235 段(text/26-fm-deriving-types-from-other-types.txt:235,搜「union of all the values」)。 ↩↩

  7. 出处:「Deriving Types from Other Types」第 68–77 段(text/26-fm-deriving-types-from-other-types.txt:68,搜「automatically be reflected」)。 ↩

  8. 出处:「Deriving Types from Other Types」第 79–88 段(text/26-fm-deriving-types-from-other-types.txt:87,搜「producer」)。 ↩

  9. 出处:「Deriving Types from Other Types」第 329–381 段(text/26-fm-deriving-types-from-other-types.txt:373,搜「keyof FormValues」)。 ↩

  10. 出处:「Deriving Types from Other Types」第 134 段(text/26-fm-deriving-types-from-other-types.txt:134,搜「objects, functions, classes」)。 ↩

  11. 出处:「Deriving Types from Other Types」第 136–153 段(text/26-fm-deriving-types-from-other-types.txt:153,搜「the other way around」)。 ↩

  12. 出处:「Deriving Types from Other Types」第 383–442 段(text/26-fm-deriving-types-from-other-types.txt:419,搜「keyof typeof configurations」)。 ↩

  13. 出处:「Deriving Types from Other Types」第 155–182 段(text/26-fm-deriving-types-from-other-types.txt:173,搜「Did you mean to retrieve」)。 ↩↩

  14. 出处:「Deriving Types from Other Types」第 184–189 段(text/26-fm-deriving-types-from-other-types.txt:187,搜「AlbumTuple[0]」)。 ↩

  15. 出处:「Deriving Types from Other Types」第 191–206 段(text/26-fm-deriving-types-from-other-types.txt:204,搜「ArtistName」)。 ↩

  16. 出处:「Deriving Types from Other Types」第 208–228 段(text/26-fm-deriving-types-from-other-types.txt:225,搜「AlbumPropertyTypes = Album」)。 ↩

  17. 出处:「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」)。 ↩

  18. 出处:「Deriving Types from Other Types」第 570–628 段(text/26-fm-deriving-types-from-other-types.txt:618,搜「IterableIterator」)。 ↩↩↩

  19. 出处:「Deriving Types from Other Types」第 630–633 段(text/26-fm-deriving-types-from-other-types.txt:630,搜「number type as the argument」)。 ↩↩

  20. 出处:「Deriving Types from Other Types」第 633 段(text/26-fm-deriving-types-from-other-types.txt:633,搜「great pattern」)。 ↩↩

  21. 出处:「Deriving Types from Other Types」第 237–263 段(text/26-fm-deriving-types-from-other-types.txt:263,搜「Plain Old JavaScript Object」)。 ↩

  22. 出处:「Deriving Types from Other Types」第 267–275 段(text/26-fm-deriving-types-from-other-types.txt:275,搜「make refactoring a little harder」)。 ↩

  23. 出处:「Deriving Types from Other Types」第 277–317 段(text/26-fm-deriving-types-from-other-types.txt:300,搜「doesn’t matter」)。 ↩

  24. 出处:「Deriving Types from Other Types」第 239–263 段(text/26-fm-deriving-types-from-other-types.txt:249,搜「as const」)。 ↩

  25. 出处:「Deriving Types from Other Types」第 319–327 段(text/26-fm-deriving-types-from-other-types.txt:327,搜「starting a project today」)。 ↩

  26. 出处:「Deriving Types from Other Types」第 325 段(text/26-fm-deriving-types-from-other-types.txt:325,搜「in-progress proposals」)。 ↩

  27. 出处:「Deriving Types from Other Types」第 639–663 段(text/26-fm-deriving-types-from-other-types.txt:656,搜「won’t work on its own」)。 ↩

  28. 出处:「Deriving Types from Other Types」第 661–663 段(text/26-fm-deriving-types-from-other-types.txt:663,搜「SellAlbumParams[1]」)。 ↩

  29. 出处:「Deriving Types from Other Types」第 676–686 段(text/26-fm-deriving-types-from-other-types.txt:680,搜「unwrap the Promise」)。 ↩

  30. 出处:「Deriving Types from Other Types」第 688–737 段(text/26-fm-deriving-types-from-other-types.txt:728,搜「EditorProps = Parameters」)。 ↩↩

  31. 出处:「Deriving Types from Other Types」第 739–811 段(text/26-fm-deriving-types-from-other-types.txt:789,搜「Parameters<typeof makeQuery>」)。 ↩

  32. 出处:「Deriving Types from Other Types」第 860–908 段(text/26-fm-deriving-types-from-other-types.txt:908,搜「more succinct solution」)。 ↩↩↩

  33. 出处:「Deriving Types from Other Types」第 914–959 段(text/26-fm-deriving-types-from-other-types.txt:951,搜「pattern matching」)。 ↩↩

  34. 出处:「Deriving Types from Other Types」第 961–980 段(text/26-fm-deriving-types-from-other-types.txt:977,搜「operates similarly」)。 ↩↩

  35. 出处:「Deriving Types from Other Types」练习 10-1 与解答(text/26-fm-deriving-types-from-other-types.txt:370,搜「use keyof to extract the keys」)。「inputs is derived from FormValues」见第 381 段。 ↩↩↩

  36. 出处:「Deriving Types from Other Types」练习 10-2 解答(text/26-fm-deriving-types-from-other-types.txt:419,搜「keyof typeof configurations」);两段式写法见第 422–423 段。 ↩↩↩

  37. 出处:「Deriving Types from Other Types」练习 10-3 解答(text/26-fm-deriving-types-from-other-types.txt:472,搜「ProgramModeMap["GROUP"]」)。 ↩↩↩↩

  38. 出处:「Deriving Types from Other Types」练习 10-4 解答(text/26-fm-deriving-types-from-other-types.txt:507,搜「PLANNED_ONE_ON_ONE」)。 ↩↩

  39. 出处:「Deriving Types from Other Types」练习 10-5 解答(text/26-fm-deriving-types-from-other-types.txt:553,搜「[keyof typeof programModeEnumMap]」);悬停结果见第 562 段。 ↩↩↩

  40. 出处:「Deriving Types from Other Types」练习 10-6 与解答(text/26-fm-deriving-types-from-other-types.txt:605,搜「IterableIterator」);[number] 解法见第 632 段(搜「all possible index values」)。 ↩↩↩↩

  41. 出处:「Deriving Types from Other Types」练习 10-7 与解答(text/26-fm-deriving-types-from-other-types.txt:784,搜「refers to a value」);Parameters<typeof makeQuery> 见第 789 段;MakeQueryParameters[1] 见第 811 段。 ↩↩↩

  42. 出处:「Deriving Types from Other Types」练习 10-8 解答(text/26-fm-deriving-types-from-other-types.txt:849,搜「ReturnType<typeof createUser>」)。 ↩↩

  43. 出处:「Deriving Types from Other Types」Awaited 一节(text/26-fm-deriving-types-from-other-types.txt:680,搜「similar to using the await」)。 ↩