编程语言研究书库

可复用的工具函数 — 泛型函数、类型谓词、断言函数与函数重载

可复用的工具函数 — 泛型函数、类型谓词、断言函数与函数重载

这一章讲什么: TypeScript 的复杂度常被说成只有两层——库开发(条件类型、 映射类型全上)和应用开发(只求类型安全,复杂类型交给库)。 原书在全书最后一章指出:还有被忽视的第三层——应用里的 /utils 文件夹: groupBy、debounce、retry 这类在整个项目里被调用几百次的工具函数, 它们是「应用内部的迷你库」。本章教你给这一层写出类型安全的函数: 泛型函数、类型谓词、断言函数、函数重载——也是全书的收束章。

1. 第三层:/utils 文件夹,和它的复杂度预算

先听原书怎么给这一层定位,因为它决定了本章所有工具的使用尺度1:

  库开发 ────────── 应用开发
  (条件类型、映射类型、(类型安全即可,复杂类型
   泛型,全副武装)        都住在引用的库里)
        ╲              ╱
         ╲            ╱
          /utils 文件夹   ← 第三层,常被忽视
          groupBy、debounce、retry:
          在一个应用内被复用几百次的迷你库

  图说:复杂度预算随层级收紧——库可以把类型系统用满,
  /utils 要用高级特性但保持可读,应用代码尽量少用。

先看原书给前两层的画像,它们共同把第三层衬了出来1:

  • 库开发那一端,「你会用上 TypeScript 最晦涩、最强大的特性」—— 条件类型、映射类型、泛型,全套都要,为的是让库足够灵活、什么场景都接得住;
  • 应用开发那一端,「你主要关心代码类型安全,类型要反映应用里真实发生的事; 复杂的类型都住在你引用的库里」——需要对 TypeScript 熟门熟路, 但高级特性几乎用不上。

主流社区的经验法则是「这对应用代码太复杂了,只有写库才用得上」。 原书的反驳很具体:应用大到一定程度,你会把常见模式沉淀成一组可复用函数—— 它们可能在大型应用里被用几百次,「就像应用范围里的迷你库」1。 理解怎么给这些函数写类型,能为团队省下大量时间: 模式被抓住了,代码更好维护、也搭得更快2。

「几百次」这个数值得停一秒。一个只被调用两次的函数,它的类型写得再精致, 收益也只惠及两个调用点;一个被调用几百次的函数,签名上的每一处精确, 都会被乘法放大几百倍——返回 any 是几百个调用点一起失守,谓词撒谎是几百个 if 一起放宽。这就是为什么本章的工具都属于这一层:不是它们难到只属于这里, 而是只有这里的复用密度,配得上它们的维护成本。反过来也成立: 如果你的项目还小,/utils 里的函数屈指可数,本章的正确读法是「知道有这些工具, 等复用真的出现再上」——复杂度预算的第一课,就是承认预算的存在。

这条定位值得当成尺子用:**/utils 里允许使用高级特性(本章会全用上), 但要服务于「几百次调用」的复用收益;只被用两三次的东西, 就别给它配一套泛型 + 谓词 + 重载——那是给库准备的仪式感,不是给业务代码的1。

2. 主线例子:getFirstElement,从 any 到泛型

先看病灶:一个看似无害的工具函数

const getFirstElement = (arr: any[]) => {
  return arr[0];
};

const first = getFirstElement([1, 2, 3]);
// 悬停 first → const first: any;

这个函数在运行时的行为完全正确,但它的返回类型是 any—— 任何用它的人都在不知不觉中退出了 TypeScript 的保护。 any 是什么、有多危险,第 04 章已经讲过:类型系统对它而言形同虚设3。

病灶的本质:函数的输入类型(any[])和返回值之间断开了联系。 传进去数字数组,它本该还给你 number;传字符串数组,本该还给你 string。 修复思路由此而定:让 TypeScript 知道数组的元素类型是什么,并拿它去标注返回的东西。 一言以蔽之——让函数泛化3。

修复:声明一个类型参数

const getFirstElement = <TMember>(arr: TMember[]) => {
  return arr[0];
};

和泛型类型同一套记号,位置不同:尖括号出现在函数名之后、参数列表之前, 声明这个函数收一个类型参数 TMember。之后的 TMember[] 把它用进参数类型—— 数组的元素类型不再是写死的 any,而是「这次调用传进来的元素类型」。 惯例上类型参数带 T 前缀,用来和普通类型区分4。

调用处发生了什么:推断

const firstNumber = getFirstElement([1, 2, 3]);
// 悬停 → const firstNumber: number | undefined;

const firstString = getFirstElement(["a", "b", "c"]);
// 悬停 → const firstString: string | undefined;

const firstAlbum = getFirstElement([
  { title: "Blue", artist: "Joni Mitchell" },
]);
// 悬停 → const firstAlbum: { title: string; artist: string; } | undefined
// (前两行调用是原书示例;第三行这个对象数组调用是我们为演示推断深度补的,
//   返回值形状是按 getFirstElement 的签名推出来的,不是原书印出的悬停结果)

没有传任何类型实参,TypeScript 自己把 TMember 推成了 number、string、对象类型。 返回值跟着输入走:数组里是数字,拿到的就是 number | undefined—— 函数从此类型安全,进什么数组,出什么元素5。 第三行还顺带演示了推断的深度:对象数组进,TMember 推成对象类型, 返回值精确到 { title: string; artist: string; }——不是 any,不是 object, 是逐字段的具体形状。这就是「输入输出连线」的完整回报。

(那个 | undefined 来自数组索引可能越界——若你的项目开了 noUncheckedIndexedAccess,第 15 章的配置项,所有索引访问都会带上它。)

推断错了怎么查:悬停函数名

复杂函数的类型推不出来时,原书给的办法是悬停函数名本身:

const first = getFirstElement([1, 2, 3]);

// 悬停 getFirstElement 显示:
const getFirstElement: <number>(arr: number[]) => number;

尖括号里就是这次调用推断出的 TMember:number——传了数字数组,推断成 number6。 参数一多、函数一复杂,这招就是排障的起点;实在不确定,还可以在文件里 临时写几个带期望类型的调用,看推断往哪偏6。

注意一个细节:这里悬停的是函数名(调用处),不是函数定义。 两处悬停各回答一个问题:调用处悬停看「这次调用推成了什么」, 定义处悬停看「模具本身什么样」——和泛型类型的「定义悬停参数、调用悬停结果」 是同一对互补动作(第 16 章第 8 节)。类型系统的输出窗口永远只有这两个。

主线例子走到这里,泛型函数的核心已经讲完: 输入类型和输出类型在签名里重新连上了,连接它们的线是类型参数。 后面三节把这根线加粗:默认值、约束,以及让函数「参与收窄」的两件工具。

顺便立一条反模式:工具函数不许吃 any[]、吐 any

getFirstElement 的病灶值得单独立碑,因为它是 /utils 层最高发的类型事故:

  写工具函数的诱惑路径              类型系统的实际遭遇
  ───────────────────────────────  ─────────────────────────────
  「什么数组都能进」→ 参数写 any[]    参数是 any[] → arr[0] 是 any
  → 返回值不管了                 → 返回值 any → 调用方全部失守
                                 → 几百次调用 = 几百处 any

  图说:any 会在复用的放大器下扩散——自己写时用一次 any 是局部事故,
  工具函数返回 any 是全体调用方的事故。这就是为什么 /utils 层
  的第一个纪律是「参数与返回值之间必须有类型连线」。

这条纪律反过来也解释了本章的存在意义:不是「泛型函数很酷所以学」, 而是不泛型就守不住这条纪律——参数和返回值之间的连线,只有类型参数能搭。 JSON.parse 那种「输入 string、输出 any」的函数,类型系统天生连不起来, 只能由调用方自己断言(第 04 章的 type faith 讨论);而你自己的工具函数, 没有任何借口7。

3. 什么样的函数算泛型函数,以及三种写法

判定标准只有一条:函数声明了类型参数,它就是泛型函数;没有,就不是。 函数能不能「收类型」完全看它自己宣没宣告,和它在标准库里多有名无关。 对照两个例子:new Set<number>([1, 2, 3]) 合法,因为 Set 的构造器声明了类型参数; 而 JSON.parse<{hello: string}>(...) 报错—— 「Expected 0 type arguments, but got 1」——因为 JSON.parse 不是泛型函数, 它声明的类型世界里没有参数可收7。

这也解释了第 04 章留过的一个问题:为什么 JSON.parse 永远返回 any,只能标注、不能传类型? 答案就在判定标准里——它的签名里没有类型参数,类型系统根本没有给你留插口。 JSON.parse<Album>(json) 那种写法,等于给一把没有锁孔的锁递钥匙: 不是钥匙不对,是锁没有孔。想给 JSON.parse 的返回值「安上」类型, 唯一的路是标注或断言(第 04 章的 type faith),代价原书也早讲过7。

一个泛型函数有三种等价写法,认得出即可:

// ① 函数声明
function identity<T>(arg: T): T {
  return arg;
}

// ② 箭头函数(注意:<T> 紧跟在等号后、参数列表前)
const identity = <T>(arg: T): T => arg;

// ③ 先定义泛型函数的类型别名,再赋值
type Identity = <T>(arg: T) => void;
const identity: Identity = (arg) => arg;

写法③ 引出一个极易混淆的对子,原书专门立了一小节8:

// 泛型函数的「类型别名」:<T> 属于函数
type Identity = <T>(arg: T) => void;
//            ^^^ 贴在参数列表前 → 参数是函数的

// 泛型「类型」:<T> 属于类型本身
type Identity<T> = (arg: T) => void;
//            ^^^ 贴在类型名后 → 参数是类型的

肉眼几乎一样,差别全在尖括号的位置:贴在函数参数列表前,参数归函数; 贴在类型名字后,参数归类型。记法还是那句——TypeScript 里一个记号的含义 不在记号本身,而在它的位置(第 16 章 extends 的多义是同一个病)。

三种写法日常怎么分工?声明式的 function 和箭头函数是主力,随项目代码风格选; 类型别名的写法(③)出现在两种场合:一是你想把「函数签名」本身当作一个值传来传去 (比如实现一个「接收某类函数、返回包装函数」的工具——练习 16-5 的 PromiseFunc 就是这个路数); 二是接口字段需要「泛型方法签名」时,字段类型只能写成这种别名形态。 记住判法就不怕:尖括号贴着谁,参数就是谁的;贴函数,每次调用各自推断; 贴类型,在类型名上一次性代入8。

一个快速自测:看到 type F<T> = (x: T) => void; 和 type G = <T>(x: T) => void;, 能立刻说出「F 是泛型类型,G 是泛型函数的类型别名;F<string> 合法, G<string> 报错(G 没有类型参数可传,参数属于它描述的函数)——能脱口而出, 这一节就算过关。

4. 不传类型实参:推断;传了冲突:报错

泛型类型与泛型函数的分水岭

上一章见过:泛型类型不传实参,直接报错(Generic type 'Array<T>' requires 1 type argument(s))—— 类型层面没有「运行时实参」可看,只能向你要9。

泛型函数完全不同:

function identity<T>(arg: T): T {
  return arg;
}
const result = identity(42); // ✅ 不传也不报错!

不传,TypeScript 会尝试从运行时实参推断类型参数。 这就是原书说的「泛型函数是 TypeScript 最好的工具之一」的原因: 同一个 identity,每次调用可以推成不同的类型—— identity("hello") 得 'hello',identity({hello: "world"}) 得那个对象的字面量类型, identity([1, 2, 3]) 得 number[]10。 函数由此「知道」自己这次在处理什么类型,并据此调整补全和报错—— 这是 TypeScript 最强也最灵活的形态10。

显式传实参 = 覆盖推断

如果偏要显式传,传的和运行时实参冲突,会怎样?

const result = identity<string>(42);
// ❌ Argument of type '42' is not assignable to parameter of type 'string'.

编译器的回答很直白:42 不是 string。因为你显式告诉它 T 是 string, 于是参数 arg 的类型从「这次的实参类型」变成了 string,42 进不去11。

为什么这个报错值得多看一眼?注意它不是「类型实参与推断冲突」这种元层面的报错, 而是一个普普通通的参数类型不匹配——identity<string> 之后, 函数签名当场退化成 (arg: string) => string,42 撞在 string 上。 换句话说,显式实参把泛型函数「降格」成了普通函数,此后的检查全是常规检查。 理解了这一层,这类报错就不再神秘:把 <string> 删掉让推断接手,或把实参改成 string, 二选一。

由此得到本章最重要的分工规则,原书给的话值得原文记住:

传类型实参,是在指示 TypeScript 覆盖推断。传了,就以它为准;不传,就以运行时实参的类型为准。12

这条规则解释了日常里最困惑的一类报错:「我给泛型函数传了类型实参,为什么反而报错了?」 ——因为你接管了推断,接管就要负责:你指定的类型必须和实参相容。 大多数时候,不传才是对的;显式传实参留给「推断推不出来」或「想强制放宽」的少数场景。

泛型类型 vs 泛型函数:一张对照表收束两章

第 16 章和第 17 章教的是同一套记号的两面,到这里可以合订了:

泛型类型(第 16 章)泛型函数(本章)
尖括号位置类型名后:ResourceStatus<T>函数名后:identity<T>
不传类型实参❌ 报错(requires N type argument(s))9✅ 从运行时实参推断10
什么时候被「求值」写类型注解的那一刻每次函数调用,各推各的
默认值同语法 T = string同语法 T = string13
约束同语法 T extends ...同语法 T extends ...14
典型住户数据形状(ResourceStatus、Nullable)工具函数(getFirstElement、safeFunction)
复合用法泛型类型包住复杂形状泛型函数把泛型类型接进调用15

两栏的「同语法」不是巧合:类型参数、实参、默认值、约束,是同一套语言机制 在两个位置上的复用——这也是「没有『一个泛型』这种东西」的另一种说法: 你学的从来不是某个叫泛型的怪物,而是「参数化」这一件事,它落在类型上是一种形态, 落在函数上是另一种形态。

5. 术语正名:没有「一个泛型」这种东西

「generics(泛型)难懂」的名声,一半来自人们把 generic 当名词乱用。 原书给了一段小小的正名,非常管用16:

const identity = <T>(arg: T) => arg;       // 有人问:泛型在哪?→ 指向 <T>
const set = new Set<number>([1, 2, 3]);    // 有人说:给 Set 传了个泛型 → 指 <number>

两个说法都把不同东西混叫成一个词。拆开,只有三个词:

术语指什么例子
类型参数(type parameter)声明里的 <T>identity<T> 里的 <T>
类型实参(type argument)使用时传进去的类型Set<number> 里的 number
泛型函数/类型/类声明了类型参数的那个东西identity、Set、ResourceStatus

把词拆开之后,「泛型」这个词就消失了——只剩下「声明参数的」和「传实参的」两件事。 原书的评语:这样拆开之后,generics 好懂得多16。

这个正名为什么值得单独一节?因为它治的是真实存在的交流事故。 看两段典型对话:

  新人:「这里为什么要加个泛型?」
  老人:「不传泛型的话类型就是 any。」
  ── 两句话里,「泛型」分别指类型参数、类型实参、泛型函数,
     听者必须自己从上下文猜,猜错就走偏。

  换成正名的说法:
  「identity 是泛型函数,声明了类型参数 T;
    不传类型实参的话,T 推断不出来,就兜成 unknown。」
  ── 三个词各就各位,没有歧义可猜。

这和张嘴闭嘴「那个泛型」的模糊语感,是同一个东西的两副面孔;也和 extends、as 的多义 一样,是「同一记号复用」这个大毛病的又一个实例。术语的价值不在精确,在于把 「猜」从对话里赶出去——一章学到最后,这个收获和任何语法同等实用。

最后把三个词钉进肌肉记忆,因为出门处处撞见:

你在读的东西该用的词
库文档里 Array<T> 的 T类型参数(type parameter)
自己写的 Set<string> 的 string类型实参(type argument)
「这个函数/类/类型声明了参数」泛型函数/类/类型
别人嘴里含糊的「这里加个泛型」请他改用上面三个词之一

英文放在括号里,是因为这三组词在库的文档、报错信息、别人的代码评审里 都以英文原样出现——认得出,才接得上话;另外注意原书给的三个词都是 「类型XX」的偏正结构,没有一个是「泛型」打头的名词,所以「传一个泛型进去」 这种说法,从今往后要知道它缺了名词。

这张表也是本章术语卡的速查版:正文展开讲了三页,真正要背的是这四行。 背完之后的一个副作用:再读库的类型声明文件(.d.ts)时, <T>、Set<K, V> 这类记号会从「密文」变回「参数表」——这正是第 14 章 「.d.ts 是给谁看的」在泛型语境下的兑现。术语正名完成,本章的主体任务也完成: 剩下的练习与综合案例,是把这三个词用熟的过程,而不是新的知识。 这也呼应了第 16 章的那句话:新能力几乎从不引入新机制,而是旧机制的组合—— 本章的谓词、断言、重载,拆开看全是「函数 + 已有类型语法」, 组合起来才成了 /utils 层的四件工具。

6. 默认值与约束:和泛型类型同语法的两件事

默认值:createSet 与 unknown 兜底

函数的类型参数也能给默认值,语法与泛型类型一致:

const createSet = <T = string>(arr?: T[]) => {
  return new Set(arr);
};

const defaultSet = createSet(); // Set<string>——不传实参,默认值接手
const numberSet = createSet<number>([1, 2, 3]); // Set<number>——传了就覆盖

默认值的使用动机和函数默认参数相同:运行时实参是可选的, 没传实参时推断没有原料,默认值顶上13。三个调用摆在一起,默认值的行为就完整了:

const createSet = <T = string>(arr?: T[]) => {
  return new Set(arr);
};

createSet();                    // ① 不传实参 → 默认值接手:Set<string>
createSet(["a", "b"]);          // ② 传实参 → 实参推断:Set<string>(元素是字符串)
createSet<number>([1, 2, 3]);   // ③ 显式实参 → 覆盖默认值:Set<number>

注意默认不是约束:createSet<number> 完全合法,说明任何类型都还能进来13。

把默认值拿掉再看一次:

const createSet = <T>(arr?: T[]) => new Set(arr);

const unknownSet = createSet(); // Set<unknown>

不传、不默认则兜底 unknown——再把这个兜底和 createSet 的默认值对照一次, 「推断 → 默认值 → unknown 兜底」的三级瀑布就完整了:

  调用时有能推断的实参? ──是──→ 用推断结果(练习 16-3 第④站)
        │否
  声明了默认值? ──是──→ 用默认值(createSet 的 T = string)
        │否
        └──────→ unknown 兜底(createSet 去掉默认值后)

  图说:三级瀑布,优先级从高到低。unknown 兜底不是错误,是「实在没辙」的诚实回答——
  它的出现意味着「该接的实参没接上」,该回头查签名,而不是拿 any 糊上。

这个「unknown 兜底」是个有用的诊断信号: 悬停看到函数返回了带 unknown 的泛型,第一反应应该是「有实参没接上推断」, 去检查是哪个参数没和类型参数挂钩(本章练习 16-3 会完整演示一次这种排查)17。

约束:removeId 与「顺带学会 Omit」

约束的语法同样照搬泛型类型:T extends ...。看一个真实的工具函数—— 拿走对象里的 id:

const removeId = <TObj>(obj: TObj) => {
  const { id, ...rest } = obj;
  //            ❌ 红线画在 id 上:Property 'id' does not exist on type 'unknown'.
  return rest;
};

错误的原因值得细嚼:不带约束的类型参数 TObj,在函数体内被当成 unknown 对待——unknown 上有什么属性?编译器不知道,也不敢让你解包14。 约束就是来给函数体内「开路」的:

const removeId = <TObj extends { id: unknown }>(obj: TObj) => {
  const { id, ...rest } = obj;
  return rest;
};

加了 extends { id: unknown },TObj 至少是「带 id 的对象」, 解构 { id, ...rest } 类型层面说得通了。约束同时在调用处站岗:

removeId({ name: "Alice" });        // ❌ 红线画在 name 上
// Object literal may only specify known properties,
// and 'name' does not exist in type '{id: unknown;}'.
// (对象字面量只能指定已知属性,而 'name' 不存在于类型 '{id: unknown;}')

removeId({ id: 1, name: "Alice" }); // ✅

第一个报错值得对照着看:传给泛型函数的实参,在约束面前也过「新鲜对象」那一关—— 带不上 id 的对象,连门都进不去。第二个调用则展示了约束的开放性: { id: 1, name: "Alice" } 比约束 { id: unknown } 多带了一个 name, 照样通过(第 16 章的 open, not closed,函数版)。

第二行的悬停结果值得停下来欣赏:

// 悬停 result →
const result: Omit<
  { id: number; name: string; },
  "id"
>;

函数没有写任何返回类型标注,TypeScript 却推出了「入参对象减去 id」—— 连 Omit 都替你想好了。原书特意点出这是在展示编译器聪明的一面14: 泛型 + 约束 + 解构剩余(rest)的组合,让返回值类型随每次调用精确变化。 这是 /utils 层工具函数的教科书形态:一个类型参数、一个约束、一行实现。

removeId 还顺带回答了「约束里的属性类型写 unknown 是笔误吗」这个疑问:不是。 { id: unknown } 的意思是「只要有 id 属性,它是什么类型都行」—— 约束关心的是「有没有」,不是「是什么」。如果写死 { id: number }, 所有 id 是 string 的对象都会被拒之门外,工具函数反而变窄了。 unknown 在这里是「最宽容的占位」,和「具体类型」之间的分寸,就是约束设计的分寸。

7. 类型谓词:让函数参与收窄

需求:一个可复用的「它是不是 Album」

第 06 章学过收窄——if 里的检查让编译器在代码块里收窄变量的类型。 但那套检查有两个局限:一是写在调用处,同一段逻辑换个地方就得抄一遍; 二是它只能收窄「这个变量」,没法把「判断是不是 Album」本身变成团队可复用的资产。 一旦「判断一个值是不是 Album」在代码里到处要用,自然想把它提成一个函数。 提,就是本章的出发点。

function isAlbum(input: unknown) {
  return (
    typeof input === "object" &&
    input !== null &&
    "id" in input &&
    "title" in input &&
    "artist" in input &&
    "year" in input
  );
}

运行时的逻辑没毛病。问题在类型层面——悬停这个函数,签名相当难看:

// 悬停 isAlbum 显示:
function isAlbum(
  input: unknown,
): input is object &
  Record<"id", unknown> &
  Record<"title", unknown> &
  Record<"artist", unknown> &
  Record<"year", unknown>;

技术上它没错(「是个对象,且带这四个属性,属性类型未知」),但对调用方几乎没帮助: 用它收窄之后,各属性的类型都是 unknown18。

第一个本能的修法,反而把收窄弄丢了

面对 unknown 属性,直觉是补更细的检查:

function isAlbum(input: unknown) {
  return (
    typeof input === "object" &&
    input !== null &&
    "id" in input &&
    /* ... */ &&
    typeof input.id === "number" &&      // 新增:连属性类型一起查
    typeof input.title === "string" &&
    /* ... */
  );
}

结果是收窄整个失效。检查一复杂,TypeScript 推断返回类型为普通 boolean—— boolean 不携带任何类型信息,if (isAlbum(x)) 里 x 还是 unknown,一夜回到解放前19。

正解:类型谓词(input is Album)

function isAlbum(input: unknown): input is Album {
  return (
    typeof input === "object" &&
    input !== null &&
    "id" in input &&
    "title" in input &&
    "artist" in input &&
    "year" in input
  );
}

返回类型从 boolean 换成了 input is Album——这就是类型谓词的语法: 函数签名里宣告「此函数返回 true 时,input 就是 Album」。 从此调用处的收窄恢复了,而且比第 06 章的原始检查更强。 对照着看:

const run = (maybeAlbum: unknown) => {
  if (isAlbum(maybeAlbum)) {
    // 没有谓词时:maybeAlbum 是「object & Record<...>」,属性全 unknown
    // 有了谓词:maybeAlbum 就是 Album,.name.toUpperCase() 顺利通过
    maybeAlbum.name.toUpperCase(); // ✅ No error!
  }
};

原书的解说值得照引:TypeScript 明白「执行到 toUpperCase 这一行, 那它就一定是专辑名」——这个模式对复杂的类型守卫可读性好得多20。 第 06 章提过这个语法,本章是它的用武之地:把一段会复用的检查,升格成编译器认可的收窄。

危险声明:谓词和 as、! 一样不可信

类型谓词有一个必须直视的性质:编译器不检查你谓词写得对不对。

function isAlbum(input): input is Album {
  return typeof input === "object";  // 只查了「是对象」,根本没查属性
}

这个谓词签名上说「true ⟹ 是 Album」,运行时才不管什么签名: 任何对象——哪怕一个属性都不带——都会让函数返回 true,然后被当成 Album 用。 原书给的风险评级很直接:类型谓词的危险程度和 as、非空断言(!)差不多—— 它们都是你对编译器说的话,编译器照单全收,运行时不兜底21。 写谓词的纪律因此和第 12 章一样:谓词里的检查,必须真的覆盖签名承诺的类型; 偷懒的检查 = 对编译器撒谎,账单照旧在运行时收。

危害的具体形状也值得想象一次:假设 isAlbum 漏查了 year, 而某处调用写 if (isAlbum(x)) console.log(x.year.toFixed())—— 类型系统全程绿灯(x 是 Album, year 是 number),运行时 x.year 是 undefined, toFixed() 当场炸。更糟的是,这个谎言会随着复用扩散: 谓词被引用的每一处 if,都继承了这个漏洞。这就是为什么「检查覆盖承诺」 不是洁癖,而是谓词这种「签名级」谎言的底线——as 撒一次谎,一个表达式买单; 谓词撒一次谎,所有调用点一起买单。

8. 断言函数:不返回 boolean,而是「不通过就抛」

与谓词的同和异

有一类场景,你不想要「if 里收窄」,你想要「过了这关,它就必须是那个类型」—— 不过关就直接抛错中断。断言函数就是为此生的。它和类型谓词的关系, 可以先一句话定位:谓词是「问」,断言函数是「赌」——问可以答否, 赌则没有「否」这条路,不对就当场掀桌(抛错)。看 isAlbum 的改写版:

function assertIsAlbum(input: unknown): asserts input is Album {
  if (
    typeof input === "object" &&
    input !== null &&
    "id" in input &&
    "title" in input &&
    "artist" in input &&
    "year" in input
  ) {
    throw new Error("Not an Album!");
  }
}

返回类型从 input is Album 换成 asserts input is Album—— 这就是断言函数的语法:函数声明自己「执行完时,参数已被断言为某类型; 若不是,函数会抛错,执行根本到不了下一行」22。

它的收窄因此更激进:不需要 if,调用本身就够了——

function getAlbumTitle(item: unknown) {
  // 此处 item 是 unknown
  assertIsAlbum(item);
  // 过了断言这行,item 已被收窄成 Album
  console.log(item.title);
}

原书的概括:相比在 if 语句里检查,函数调用本身就足以断言 input 是 Album23。 适合「值不对就没必要继续」的入口把关场景。和谓词对照,控制流的差别一目了然: 谓词让调用方分岔(if/else 两条路),断言函数让调用方直行 (过了这行,类型就定了)——这也是为什么 §8 的选择表把「失败怎么办」当作分野。

顺带一提,断言函数和 JavaScript 里的「断言式错误处理」是同一个哲学: 与其让错误默默流下去,不如在边界处大声失败(fail fast)。TypeScript 的 asserts x is T 只是给这个哲学补上了类型层面的后半句—— 「大声失败的地方,之后的代码可以当作 x 一定是 T 来写」。 原书没有展开这层联系(补充,不在书里,来自通用知识)。

断言函数的专属坑:typeof null === "object"

断言函数和谓词共享同一个危险源(签名不受运行时检查),还多一个 JavaScript 的 经典暗坑。看一个偷懒实现的后果:

function assertIsAlbum(input: unknown): asserts input is Album {
  if (typeof input === "object") {
    throw new Error("Not an Album!");
  }
}

let item = null;

assertIsAlbum(item);
// 过了断言,item 被收窄成 Album——可它在运行时明明是 null!
item.title; // 💥 运行时报错

assertIsAlbum(null) 没有抛错——因为 typeof null === 'object' 是 JavaScript 著名的老怪癖,检查被 null 骗过去了;断言函数一通过,编译器放心把 item 当 Album, 运行时就炸了24。这正是原书拿来强调的教训:断言函数检查不全,运行时错误等着你24; 而上一个例子的正确实现里,input !== null 那一行就是防这个坑的—— 写任何「typeof x === 'object'」式的断言,null 检查必须同场出现22。

谓词还是断言函数:怎么选

两件工具干的是同一件事(把运行时检查接进类型系统),用法分野在「失败时怎么办」:

类型谓词 input is T断言函数 asserts input is T
返回值boolean(进 if)无(函数体内抛错)
收窄方式if 块内收窄调用那一行之后收窄23
失败时走 else,程序继续抛错,程序中断
适用场景「是 T 就走 A 路,不是就走 B 路」「不是 T 就没法往下走,直接炸」
写错的代价与 as、! 同级21与 as、! 同级,另加 null 暗坑24

选择的经验锚点是调用方的代码形状:调用方需要「两条路都继续」就选谓词, 需要「一条道走到黑」就选断言函数。练习 16-7 里 handleRequest 的场景 (请求处理到一半,不是管理员就没法处理)是断言函数的主场; 而 isAlbum 那种「是专辑就听、不是就跳过」是谓词的主场。 两件工具的危险等级相同,选择不该基于「哪个更安全」,只该基于控制流。

9. 函数重载:一副实现,多副签名

需求:两种调用方式,一个函数

想象 searchMusic 支持两种调用:老代码传三个参数(artist、genre、year), 新代码传一个对象——为了照顾历史原因,两种姿势都得留着25。 运行时用 if 分流很容易,类型上怎么表达「两种合法姿势」? 函数重载——给同一个实现定义多副调用签名25:

// 重载签名 ①:三个参数
function searchMusic(artist: string, genre: string, year: number): void;

// 重载签名 ②:一个对象
function searchMusic(criteria: {
  artist: string;
  genre: string;
  year: number;
}): void;

// ❌ 此刻报错:Function implementation is missing or not
//    immediately following the declaration.

注意需求本身的形状:这是「参数个数不同」的双形态——三参数 vs 单参数。 个数不同正是重载的主场(对照第 9 节末尾:个数相同该用并集)26。 只有签名没有实现,编译器不干。报错信息本身就是概念解释: 重载签名(overload signature)只是「声明调用方式」, 实现签名(implementation signature)才是带逻辑的真身, 而且它必须紧跟在重载声明之后、并且与所有重载相容27。

实现签名:把重载收成一根并集参数

function searchMusic(artist: string, genre: string, year: number): void;
function searchMusic(criteria: {
  artist: string;
  genre: string;
  year: number;
}): void;

function searchMusic(
  artistOrCriteria: string | { artist: string; genre: string; year: number },
  genre?: string,
  year?: number,
): void {
  if (typeof artistOrCriteria === "string") {
    // 按三个参数搜
    search(artistOrCriteria, genre, year);
  } else {
    // 按对象搜
    search(
      artistOrCriteria.artist,
      artistOrCriteria.genre,
      artistOrCriteria.year,
    );
  }
}

实现签名把两副重载折成一个参数:第一个参数是「字符串或对象」的并集, 后两个参数变成可选。函数体里照旧 typeof 分流——类型层面和运行时是同一次分岔28。

两种调用都通过,而「混搭」被拦:

searchMusic("King Gizzard and the Lizard Wizard", "Psychedelic Rock", 2021); // ✅
searchMusic({ artist: "Tame Impala", genre: "Psychedelic Rock", year: 2015 }); // ✅

searchMusic({ artist: "Tame Impala", genre: "Psychedelic Rock", year: 2015 },
  "Psychedelic Rock"); // ❌
// No overload expects 2 arguments, but overloads do exist that
// expect either 1 or 3 arguments.
// (没有接受 2 个参数的重载;现有重载只接受 1 个或 3 个)

报错信息把合法姿势报得清清楚楚——这也是重载的日常体验: 它不只是放行合法调用,还用「最接近的重载」去解释非法调用29。 对比普通函数遇到错参时的报错(「参数类型不匹配」一类), 重载的报错是「没有任何一副签名接受这个调用,而现有的签名接受的是 1 个或 3 个参数」—— 信息量天差地别:它在告诉你这个函数的合法调用形态有哪几种, 等于把重载签名们当文档读了。这也是为什么 API 作者愿意为重载付维护费: 报错本身成了调用指南。

重载 vs 并集:什么时候别用重载

原书给了一条非常实用的划界:参数个数不同,用重载;参数个数相同、只是类型不同,用并集26。 如果 searchMusic 只想支持「字符串或对象、都是单参数」,并集就够了:

function searchMusic(
  query: string | { artist: string; genre: string; year: number },
): void {
  if (typeof query === "string") {
    searchByArtist(query);
  } else {
    search(query.artist, query.genre, query.year);
  }
}

原书的评语:这比两副重载 + 一个实现省行得多26。

实现签名是「后台」,不是「门面」

重载有一个必须知道的可见性规则:调用方只能看到重载签名,看不到实现签名。 上面例子的实现签名长这样:

function searchMusic(
  artistOrCriteria: string | { artist: string; genre: string; year: number },
  genre?: string,
  year?: number,
): void { /* ... */ }

它的后两个参数是可选的——如果只看得见这副签名, searchMusic("artist", "genre")(两个参数)似乎也该合法。 但它不合法:两个参数的调用被重载签名们拒绝, 「No overload expects 2 arguments」29。 实现签名的宽,只宽给函数体内部;调用方被重载签名管着。

这个规则带来一条维护纪律:改函数时,光改实现签名不够—— 重载签名才是对外的承诺,实现签名只是兑现承诺的后台。 重载签名的每一个参数组合,实现签名都必须接得住(编译器会检查「实现与所有重载相容」), 这就是第 25 段那句「must be compatible with all the defined overloads」的日常含义27。

合起来,重载真正的用武之地是「调用形态(参数个数/顺序)不同」的场景; 「同一形态、类型有别」的并集能接住时,别为上重叠的仪式感付维护费。 这也是本章复杂度预算的最后一道闸。

10. 七个练习:从「会用」到「会写」

本章练习的梯度设计得很清楚:16-1 到 16-3 练泛型函数的基本功(声明、默认值、推断), 16-4 到 16-6 练约束与泛型类型/函数的组合,16-7 收官断言函数。 和前面各章的练习不同,这一组的每一步都在往「一个能进 /utils 的真函数」靠近—— createStringMap、uniqueArray、addCodeToError、safeFunction, 每一个都长得像你明天就要在代码库里写的东西。 逐个走一遍,解法里只保留「绕不过去的那一步」,和正文重复的部分一笔带过。

createStringMap:造一个泛型函数(练习 16-1)

需求:一个函数,返回「键是 string 的 Map,值类型由调用方指定」:

const createStringMap = () => {
  return new Map();
};
// 现状:返回 Map<any, any>——和 getFirstElement 同款 any 病灶

目标调用长这样:createStringMap<number>() 之后 numberMap.set("foo", 123) 合法、 numberMap.set("bar", true) 报错(练习里用 @ts-expect-error 断言了这条报错必须发生); 传对象类型实参 createStringMap<{a: number}>() 时,set("foo", {b: 123}) 这种 差一个属性的值也得被拦;不传实参时,Map 值退成 unknown30。 三组测试对应三种能力:显式实参生效、精确形状校验、兜底未知——一个练习把泛型函数的 三面都验了一遍。

改法两步,和 PromiseFunc(第 16 章)同构:

第 ① 步:加类型参数。 const createStringMap = <T>() => { return new Map(); }; 调用处的报错消失了,但悬停函数——还是 Map<any, any>: 参数声明了,没接进返回值31。

第 ② 步:把参数接进 new Map。 键的类型是钉死的 string,值的类型用 T:

const createStringMap = <T>() => {
  return new Map<string, T>();
};

此后 createStringMap<number>() 给出 Map<string, number>, createStringMap<{a: number}>() 给出 Map<string, {a: number}>—— set 错值类型当场报错;不传实参时 T 无从推断,兜底 unknown: 悬停 Map<string, unknown>,和练习目标逐字相符32。

这个练习浓缩了泛型函数的全部基本功:参数声明 → 接进签名 → 调用处验证。 而那一步「参数加了但返回值还是 any」的中间态,正是 unknown 兜底信号的第一印象—— 悬停看到 <T> 出现了而 any 没走,就知道接线还差一段。

默认实参练习(16-2):T = string

在 16-1 的基础上,要求不传实参时 Map 的值默认 string(而不是 unknown):

const createStringMap = <T = string>() => {
  return new Map<string, T>();
};

const stringMap = createStringMap(); // Map<string, string>
stringMap.set("bar", 123);           // ❌ 现在 number 也进不来了
const numberMap = createStringMap<number>(); // Map<string, number>——默认值照常被覆盖

练习的题眼在最后一行:默认值不是约束——显式传实参的优先级高于默认值, 这和泛型类型那一章的结论完全一致33。一个语法,两套场景(类型与函数)通用。

这个对称值得记住,因为学习成本直接减半:第 16 章为泛型类型学的 = 默认值 和 extends 约束,在泛型函数里原样搬过来,一个标点都不用改。 原书在多处强调了这种对应(「The syntax...is the same as for generic types」), 本章就不再当新知识教——这正是「同一个机制、两个位置」这条主线的又一次兑现。

uniqueArray:一次完整的推断排查(练习 16-3)

需求:数组去重函数,测试要求不传类型实参时, 数字数组进出得 number[]、字符串数组进出得 string[]34:

const uniqueArray = (arr: any[]) => {
  return Array.from(new Set(arr));
};

改对这道题要走完一条排查链,每一站都对应一种「推断没接上」的典型症状:

第 ① 站:只加类型参数。 const uniqueArray = <T>(arr: any[]) => { ... }; 不传实参时 T 无源可推 → 兜底 unknown;悬停调用处,签名是 <unknown>(arr: any[]) => any[]——返回的 any[] 病灶原封不动35。

第 ② 站:加返回类型标注 T[]。 想「把 T 安在返回值上」:

const uniqueArray = <T>(arr: any[]): T[] => { ... };

悬停变成 (arr: any[]) => unknown[]——T 仍然没推断源,标注只让「unknown 数组」 变得名正言顺。传给 number[] 测试依旧失败35。

第 ③ 站(验证机制):显式传实参。 uniqueArray<number>([1,1,2,3,4,4,5]) 悬停 result → number[]。机制通了:显式实参能覆盖一切; 但测试明确不传实参,所以这站只是证明「参数活着」,不是终点35。

第 ④ 站(正解):把参数也换成 T[]。

const uniqueArray = <T>(arr: T[]): T[] => {
  return Array.from(new Set(arr));
};

T 同时出现在参数和返回位置上,推断的回路闭合: 传 number[] → T 推成 number → 返回 number[];传 string[] → 返回 string[]。 测试全过36。

这站的总结论,原书写得明明白白: 显式传了类型实参,TypeScript 就用它;不传,就从运行时实参推断37—— 第 4 节的分工规则,在这里被完整演示了一遍。 更值钱的是排查链本身:unknown 兜底、T[] 标注无用、显式实参验证, 三站症状各有指向,是以后诊断任何泛型函数的通用路数。

把三站浓缩成一张排障卡,挂在工具箱里:

  泛型函数行为不对?
    │
    ├─ 悬停签名里类型参数是 unknown?
    │    └─ 推断回路没闭合:检查每个类型参数是否至少出现在一个「运行时实参」的位置上
    │       (uniqueArray 第②站的教训:只标返回类型 T[] 闭不了环)
    │
    ├─ 显式传了实参就不对、不传就对?
    │    └─ 你在覆盖推断:确认显式类型与实参相容(第 4 节的分工规则)
    │
    └─ 函数体内属性打不开(unknown)?
         └─ 缺约束:给类型参数加 extends(第 6 节 removeId 的教训)

  图说:三站覆盖「入、出、体」三个环节——进不来、出不对、体内动不了,
  各对应一种修法。这三站几乎就是泛型函数的全部典型故障。

addCodeToError:约束的实战(练习 16-4)

需求:给错误对象补一个 code 字段(缺省填 8000),函数要对任意错误类型工作:

const UNKNOWN_CODE = 8000;

const addCodeToError = <TError>(error: TError) => {
  return {
    ...error,
    code: error.code ?? UNKNOWN_CODE, // ❌ Property 'code' does not exist on type 'TError'.
  };
};

错误模式和 removeId 一模一样:裸类型参数在函数体内是 unknown, error.code 无从谈起。约束开路:

const addCodeToError = <TError extends { message: string; code?: number }>(
  error: TError,
) => {
  return {
    ...error,
    code: error.code ?? UNKNOWN_CODE,
  };
};

约束宣告两件事:message 必须有(所有错误都带消息),code 可有可无(可选, 所以 error.code 的类型是 number | undefined,?? 兜底成立)38。

测试期望也揭示了这个函数的类型形状:

addCodeToError(new Error("Oh dear!"));
// → Error & { code: number }——Error 的所有成员都在,只是多了一个 code

addCodeToError({ message: "Oh no!", code: 123, filepath: "/" });
// → { message: string; code: number; filepath: string } & { code: number }

两个测试的期望类型,都是对「入参 + code」的交叉——原书没有逐字展开这个推导, 但它成立的前提正是约束:TError extends { message: string; code?: number } 让展开操作 ...error 保留入参的全部成员,code: error.code ?? UNKNOWN_CODE 给出 number 兜底38。

测试里有个细节值得注意:传 {message, code: 123, filepath: "/"} 这种多带属性的对象 照样通过——extends 约束是开放的,超额属性照收,这正是第 16 章那句 open, not closed 的函数版38。

safeFunction:泛型类型 × 泛型函数的组合(练习 16-5、16-6)

这是全书最后一组综合练习,把第 16 章和第 17 章的工具装进同一个函数。 需求:包装一个会抛错的异步函数,包装后的版本把错误「变成返回值」:

type PromiseFunc = () => Promise<any>;

const safeFunction = (func: PromiseFunc) => async () => {
  try {
    const result = await func();
    return result;
  } catch (e) {
    if (e instanceof Error) {
      return e;
    }
    throw e;
  }
};
// 现状:返回 Promise<any>——「成功值或 Error」的信息全丢

函数体的控制流值得先读懂,因为它是这个类型问题的根源: try 里成功就返回结果,catch 里只有 Error 会被「收编」成返回值, 非 Error 的异常照旧抛出(throw e)——所以包装后函数的返回, 必然是「原函数的成功值」或「Error」的二选一39。 Promise<any> 的问题在于,它把这个二选一糊成了一团:any 里什么都有, 调用方既不知道成功值是什么类型,也不知道 Error 被混了进来。 修复的方向:让「成功值的类型」成为参数,让返回类型自己长出来。

练习 16-5 两步:

第 ① 步:PromiseFunc 泛型化。

type PromiseFunc<TResult> = () => Promise<TResult>;

把 any 换成 TResult——泛型类型一章的基本操作,但注意它发生在类型别名里: 形状先住在别名中,函数签名保持干净。

第 ② 步:safeFunction 声明自己的类型参数,把它传进 PromiseFunc。

const safeFunction =
  <TResult>(func: PromiseFunc<TResult>) =>
  async () => { /* 原实现不动 */ };

safeFunction 的尖括号声明 TResult,参数类型 PromiseFunc<TResult> 把它「传入」别名—— 泛型类型收实参的语法,在函数签名里照常工作。悬停调用处:

const safeFunction: <number>(func: PromiseFunc<number>) =>
  Promise<() => Promise<number | Error>>;

TResult 从传入的函数推断成 number——返回类型自动是 number | Error39。 原书点出组合的价值:泛型函数 + 泛型类型混用,能让泛型函数好读得多—— 复杂的部分住在类型别名里,函数签名只剩一行15。

这个「好读」值得量化一下。如果不借助 PromiseFunc 别名,safeFunction 的完整签名 得 inline 写成:

const safeFunction =
  <TResult>(func: () => Promise<TResult>) =>
  async () => { /* ... */ };

功能一样,但当参数类型再复杂一点(比如 16-6 加上 ...args: TArgs), inline 版会迅速膨胀到一行装不下。别名版把「这个函数长什么样」的知识 从签名里抽走、单独命名,签名的阅读负担立刻减半——这就是第 16 章「类型是文档」 在函数签名上的应用。

练习 16-6 加难度:被包装的函数带参数。 实现里 async (...args: any[]) 意味着包装后的函数「收任意多个 any 参数」—— (name: string) => Promise<...> 传进来,返回的函数签名全丢。修法是给 PromiseFunc 再加一个类型参数收参数列表:

type PromiseFunc<TArgs extends any[], TResult> = (
  ...args: TArgs
) => Promise<TResult>;

注意约束:TArgs extends any[]——必须是数组(参数列表本质是元组)。 原书特意说明:这里只能用 any[],试 unknown[] 不行——这个约束检查的是 「能不能展开成参数列表」,any 数组是唯一过关的形状40。

safeFunction 同步加参:

const safeFunction =
  <TArgs extends any[], TResult>(func: PromiseFunc<TArgs, TResult>) =>
  async (...args: TArgs) => { /* ... */ };

TArgs 同时出现在 func 的参数位和返回函数的 ...args 位,推断回路再次闭合: 传 (name: string) => ... → TArgs 推成 [name: string] → 返回 (name: string) => Promise<Error | string>。全部测试通过41。

这两个练习放在一起,就是 /utils 层工具的完整诞生记: 第 16 章的泛型类型负责「住下形状」,第 17 章的泛型函数负责「把形状接上调用」, 两个类型参数各自钉住一段推断回路。 这幕组合戏,是全书最后也是最高的一次合奏。

assertIsAdminUser:断言函数收官(练习 16-7)

需求:一个把关函数——参数是 User 或 AdminUser,没有 roles 属性就抛错; 过了这关,类型要收窄到 AdminUser:

interface User {
  id: string;
  name: string;
}

interface AdminUser extends User {
  roles: string[];
}

function assertIsAdminUser(user: User | AdminUser) {
  if (!("roles" in user)) {
    throw new Error("User is not an admin");
  }
}
// 现状:调用后 user 仍是 User | AdminUser——没有任何收窄发生

顺手复习一个第 06 章的旧工具:函数体现在写的 "roles" in user, 本身就是 in 运算符收窄的运行时版本——roles 在就放行,不在就抛错。 这个检查在运行时完全自洽,问题只出在类型层面没被告知: 函数体一结束,TypeScript 对 user 的认知照旧是 User | AdminUser。 断言函数要补的,正是这个「执行过后」的信息。

测试用两个 Equal 断言把这个需求钉死了:调用前 user 的类型必须是 User | AdminUser,调用后必须是 AdminUser42。 第一断言现在就能过;第二断言连同 user.roles 的访问一起报错—— 收窄没有发生,一切都如「现状」所愿地坏着。

先试类型谓词的写法 user is AdminUser,立刻撞上新报错: 「声明的类型既不是 undefined、void、any 的函数必须返回一个值」—— 谓词要求返回 boolean,而断言函数的函数体是「要么抛、要么正常结束」,没有返回值43。 这正是两个语法的分岔点,原书用这个报错把它讲得最透。

正解:换上 asserts 关键字。

function assertIsAdminUser(
  user: User | AdminUser,
): asserts user is AdminUser {
  if (!("roles" in user)) {
    throw new Error("User is not an admin");
  }
}

调用处:

const handleRequest = (user: User | AdminUser) => {
  assertIsAdminUser(user);
  // 过了这行,user 收窄成 AdminUser
  user.roles; // ✅
};

测试通过,悬停 roles → user: AdminUser42。

作为全书最后一个练习,它的位置很讲究:类型谓词与断言函数的区别 (返回 boolean、要 if / 不返回、调用即断言),到此刻已经被需求和报错一起逼了出来—— 不是背出来的,是用出来的。

练习收束:给工具函数写签名的五条纪律

七个练习走完,把散落各处的纪律收成一张卡,这是本章给 /utils 文件夹的交付物:

  1. 参数与返回值之间必须有类型连线。 连线的材料是类型参数; 连不上的函数(像 JSON.parse)只许引用,不许模仿——自己的工具函数返回 any 是全体调用方的事故。
  2. 不传类型实参是默认姿势。 显式传 = 接管推断 = 为相容性负责; 只在「推断推不出」或「要强制放宽」时接管12。
  3. 约束用来给函数体内开路,顺带在调用站岗。 裸类型参数在函数体内是 unknown, 打不开属性;extends 一上,解构、索引都活,调用处还多一层校验1438。
  4. 谓词/断言是「签名承诺」,检查必须兑现承诺。 签名与检查不符 = 对编译器撒谎, 危险级与 as、! 同级;typeof x === "object" 的同场必须排除 null2124。
  5. 重载是最后手段。 参数个数不同才上重载;个数相同先写并集, 并集写不下去再回头考虑重载26。

这五条按「危害面」排序:违反第 1 条污染全项目,违反第 4 条埋运行时炸弹, 违反第 5 条只多付维护费。写工具函数时从第 1 条开始自查,时间不够也至少守住前两条。

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

本章是终章,收束的方式不是总结,而是把全书工具装进一个文件夹:

  • 第 04 章的 any 警告、第 13 章的 unknown,在这里变成「工具函数不许返回 any」的纪律;
  • 第 06 章的收窄,在这里升格成类型谓词和断言函数——检查逻辑本身成了类型系统的一部分;
  • 第 16 章的泛型类型,在这里与泛型函数合流(safeFunction 的组合);
  • 第 12 章「对编译器撒谎」的账单,在这里被原书亲自援引——类型谓词和 as、! 同级危险21;
  • 第 15 章的 strict 与配置,是这一切的开关底座。

原书小结给这一层下了最终定义:实用工具文件夹开发,是被忽视的中间地带—— 介于简单应用代码和复杂库开发之间;像 groupBy、debounce 这样的工具函数 在整个大型应用里被反复复用,因此配得上 TypeScript 的高级特性44。

最后把这个闭环画完。全书从「类型是工具」出发,绕了一大圈,落在一个文件夹上:

  类型是工具(第 01 章的纠正)
      → 工具需要「看懂」代码(推断、收窄:第 04–06 章)
      → 看懂之后是「说清」(注解、断言、可变性:第 08–13 章)
      → 说清之后是「配好」(模块、tsconfig:第 14–15 章)
      → 配好之后是「造机器」(泛型类型:第 16 章)
      → 造机器之后,机器住进 /utils(本章)

  图说:每章都是上一步的「然后呢」。终点不是「会写类型」,
  而是「团队里那组被调用几百次的函数,签名是安全的、可读的、有语义的」。

全书到此合上:从「类型是工具」的第一页,到「为工具写类型」的最后一章, 那个闭环是完整的。

对「编程语言与系统研究」的读者,这本书的最后两章还另有一层样本价值: 它们是「一门语言的类型系统如何被设计成可编程」的最朴素演示—— 泛型类型是函数、映射类型是循环、条件类型是分支(第 16 章), 再把这套可编程的类型接到函数签名上、让推断自动运转(本章)。 全程没有形式化的记号,学习靠「悬停看结果」驱动—— 这是类型系统教学里少见的「即时反馈」路径,值得在研究笔记里单独记一笔。 原书没有从这个视角自评过(它是教材,不是论文),这层观察是我们加的, 依据只是两章的实际教法。

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

书里有据的:

  • /utils 是被忽视的第三层,复杂度预算介于库与应用之间——原书开篇的立论1。
  • 泛型函数不传类型实参时从运行时实参推断;显式传实参会覆盖推断——两个方向都有原文条款1012。
  • 类型谓词「和 as、! 差不多危险」——原书的风险评级原文21。
  • 断言函数比谓词更激进(调用本身即断言),检查不全会招致运行时错误——含 typeof null === 'object' 的实例2324。
  • 重载适合参数个数不同的场景,个数相同应改用并集——原书的划界26。
  • 实现签名必须与所有重载相容,且对调用方不可见——原文有条款27。
  • 「类型参数不归名词 generic 管」的术语三分法——原书专节正名16。
  • 约束给函数体内开路、在调用处站岗的双重作用,及 Omit 推断的「聪明」——removeId 全案14。
  • 条件类型「应用极少需要」的第 16 章划界,在本章再次得到确认:safeFunction 组合练习里刻意没用条件类型,只用约束和推断15。

原书没说的(我们补):

  • 「复杂度预算」这个术语是我们对开篇立论的命名;原书只有描述,没有术语。
  • 「悬停看到 unknown 兜底 ⟹ 有实参没接上推断」这个诊断信号是我们的提炼, 原书在练习 16-3 里示范了全过程但没有总结成信号35。
  • 「T 前缀惯例」原书有但着墨极轻(第 164 段),我们把它并进了术语卡。
  • 「谓词 vs 断言函数的选择只看控制流」「实现签名是后台不是门面」的表述是我们的归纳, 依据都是原书的示例与条款,判断本身是我们的。
  • 「Node assert 等生态库使用 asserts 签名」是补充(不在书里,来自通用知识)。

判断(我们的,不是书里的): 本章真正的分水岭不是「会不会写泛型函数」, 而是愿不愿意让函数的签名承载语义。isAlbum 加不加 : input is Album, 运行时代码一模一样;区别只在于「检查的结论」有没有被编译器听见。 /utils 层函数被调用几百次,签名的语义杠杆最大——这正是原书把高级特性 划给这一层的真实理由:不是这里难,是这里收益/成本比最高。 如果错,会错在: 如果项目里 /utils 函数的调用方很少(小应用、快速原型), 语义杠杆撑不起谓词/重载的维护成本,这条「分水岭」就不成立—— 那时本章的正确用法是只取泛型函数一节,其余三章略读。

判断(我们的,不是书里的): 类型谓词被原书与 as、! 并列警示,但三者的 「撒谎成本」其实不同:as 和 ! 的谎言影响一条表达式,谓词的谎言影响所有调用点 ——一处写错的谓词,等于在 N 个 if 里同时放宽了检查,危害面最大。 因此谓词应该比 as 用得更克制:只在「检查逻辑真的会复用」时升格,一次性场景 宁可在调用处手写 if + typeof。 如果错,会错在: 如果谓词所在的项目有完善的运行时校验层 (比如用 schema 库同时生成类型和校验器),谓词的谎言会被校验层兜住, 危害面的论证就弱很多——但那是另一种架构选择,不是谓词本身变安全了。

判断(我们的,不是书里的): 函数重载在 2026 年的新代码里应该当作 「兼容旧接口的遗产工具」,而不是「设计新 API 的工具」。 原书示范的动机(legacy 原因要兼容两种调用姿势)恰恰说明了这一点: 新写的函数如果一开始就需要重载才能表达签名,多半说明参数形态没设计好—— 要么该用并集(参数个数相同),要么该用 options 对象(参数多且部分可选, 正是重载最易退化的形态)。重载留给「老接口改不得」的场景,是它的善终。 如果错,会错在: 有些 API 的调用形态确实天然多元(比如字符串格式化函数 对不同类型参数有不同返回),并集表达不了「返回类型随参数种类分叉」—— 但那种场景往往已经站在库开发的门口,超出 /utils 层本章的适用范围。

13. 边界与局限

  • 泛型函数的推断不是全能的。 它从「实参类型」反推,推断不出「实参之外的意图」—— 练习 16-3 第 ② 站就是反例:参数还挂着 any[] 时,返回标注 T[] 什么也救不了。 显式传实参是逃生门,但接管就要负责(第 4 节)。 还有一个推论值得记住:推断以「声明了类型参数的位置」为界——类型参数 出现在哪个参数上,推断就只听哪个参数的;想从多个实参联合推断, 就得让类型参数同时出现在多个参数上(safeFunction 的 TArgs 和 TResult 各管一段)。 推断的「回路」是签名设计出来的,不是编译器猜出来的。
  • 类型谓词、断言函数都运行在「编译器信任」上。 它们的本体只是签名, 运行时的检查函数你自己写、自己负责;签名与检查不符时,没有任何机制报警—— 这是第 12 章「类型擦除」大旗下的又一个具体表现。
  • 函数重载的可维护性有隐性成本。 每副重载签名都是一份要永久维护的「公开承诺」; 实现签名对调用方不可见(调用方只看重载),改实现时容易和重载脱节—— 重载越多,这种漂移风险越大,这也是「能并集就并集」的深层理由26。
  • 本章没有覆盖的: 泛型函数里的 infer 推断、可变参数元组 (...args: TArgs 的进阶玩法)、this 参数——都属于库开发层,原书刻意停在 「/utils 够用」的边界上,和「不覆盖递归条件类型」是同一个取舍44。
  • 断言函数在生态里的位置(补充,不在书里): Node.js 自带的 assert 模块、 主流测试框架里的 assert.ok 等,在它们的类型声明里用的就是 asserts x is T 一族签名——学完本章再去看这些库的 .d.ts,会认出本章的语法原样躺在那里 (这正是第 14 章「.d.ts 是给谁看的」的一个活样本)。

14. 可带走的

把本章折进 /utils 文件夹的视角,这 12 条按「什么时候用得上」分三层。 层与层之间是递进关系:第一层保平安,第二层让签名开口说话,第三层让签名值得被复用—— 别急着上第三层,复用没出现之前,第一层已经够用。这份克制,正是本章开头那张 复杂度预算图在「可带走的」里的最后一次现身:类型是工具,工具服务于人, 预算永远由「谁在用、用多少次」来定。

  1. /utils 是第三层:复杂度预算介于库与应用之间,复用次数是唯一的拨款依据1。
  2. 泛型函数 = 声明了类型参数的函数;不传类型实参,TypeScript 从运行时实参推断——这是它和泛型类型的关键差异10。
  3. 传类型实参 = 覆盖推断:传了以它为准(冲突就报错),不传以实参为准;多数时候别传12。
  4. 术语卡:类型参数(声明的 <T>)/ 类型实参(传入的 number)/ 泛型函数(声明了前者的函数)。「一个泛型」不是合法名词16。
  5. <T> 贴在参数列表前 = 函数的类型参数;贴在类型名后 = 类型的参数。位置决定归属8。
  6. 默认值(T = string)管「不传」,约束(T extends {...})管「传错」,互不干扰;不传不默认则兜底 unknown——悬停见 unknown,查推断回路1738。
  7. 约束是开放的:超额属性照收;但它能让函数体内安全地解构属性(removeId、addCodeToError)1438。
  8. 类型谓词 input is T 让函数参与收窄;检查必须真的覆盖签名承诺,危险级与 as、! 相当2021。
  9. 断言函数 asserts input is T:不返回 boolean,调用本身即断言;typeof x === "object" 的检查必须同场排除 null2224。
  10. 重载 = 多副重载签名 + 一个实现签名;参数个数不同用重载,个数相同用并集2526。
  11. 排障三站:unknown 兜底(参数没接 T)→ 标注无用(回路没闭)→ 显式实参(验证参数活着);uniqueArray 的完整演示3536。
  12. 复杂形状住进类型别名,函数签名保持一行——泛型类型与泛型函数的组合是 /utils 工具的标准形态15。

这 12 条按使用频率排了个序:前 5 条是「每次写泛型函数都会用到」的, 6–9 条是「写检查类函数时」的,10–12 条是「真正开始造工具时」的。 日常工作守住前 5 条,就已经能把 /utils 层的大部分类型事故挡在门外; 后 7 条是团队工具开始复用、签名开始承载语义之后,才需要逐项启用的进阶纪律。

读完本章回到自己的代码库,可以做一个很轻的动作:打开项目的 /utils 目录 (或者任何沉淀公共函数的地方),逐个悬停它们的签名,看返回值里有没有 any、 参数和返回值之间有没有连线。这个动作不需要改任何代码,就会告诉你 本章的工具里,哪一个明天就用得上。

如果只能记住本章一句话,记住这句:工具函数的签名不是 bureaucratic overhead(官僚式负担), 而是它唯一能被几百个调用点共享的部分——实现只跑一次,签名被读几百次。 花一小时把签名写对,比花一小时优化实现,回报高得多。

15. 原文地图

一个使用说明:本章的前半(第 1–9 节)按原书的教学顺序重排过——原书把 「类型谓词、断言函数、重载」放在泛型函数的细节之后,我们把「/utils 定位」 提到最前,因为那是本章所有工具的预算来源;原文地图里的行号不受重排影响, 逐条对应原书第 16 章的转码文本。

本章所有「原书位置」均指原书第 16 章(Building Powerful Shared Utilities)的转码文本; 行号是 library/total-typescript-essentials/text/32-fm-generic-functions.txt 的行号, 搜词逐字取自原文,可用 node scripts/book-verify.mjs total-typescript-essentials 回核。 「同上」指上一行的同一原书章(Generic Functions)。这是全书的最后一个原文地图—— 十七章的拆解到此全部讲完。

主题原书位置原文位置
/utils 第三层定位Generic Functionstext/32-fm-generic-functions.txt:12(搜「third level」) · text/32-fm-generic-functions.txt:14(搜「groupBy」)
泛型函数判定(JSON.parse 反例)同上text/32-fm-generic-functions.txt:34(搜「Expected 0 type arguments」)
identity 三种写法同上text/32-fm-generic-functions.txt:40(搜「function identity<T>」) · text/32-fm-generic-functions.txt:50(搜「type Identity = <T>」)
函数别名 vs 泛型类型(位置)同上text/32-fm-generic-functions.txt:65(搜「belongs to the function」) · text/32-fm-generic-functions.txt:72(搜「position of the type parameter」)
实参必传(Array 报错)与函数可省同上text/32-fm-generic-functions.txt:83(搜「requires 1 type argument」) · text/32-fm-generic-functions.txt:92(搜「No error」)
从运行时实参推断同上text/32-fm-generic-functions.txt:94(搜「infer it from the function」) · text/32-fm-generic-functions.txt:98(搜「result1」)
冲突报错与覆盖推断同上text/32-fm-generic-functions.txt:111(搜「not assignable to parameter of type」) · text/32-fm-generic-functions.txt:115(搜「source of truth」)
术语三分法同上text/32-fm-generic-functions.txt:129(搜「Type parameter」) · text/32-fm-generic-functions.txt:135(搜「much easier to understand」)
getFirstElement 病灶与修复同上text/32-fm-generic-functions.txt:152(搜「const first: any」) · text/32-fm-generic-functions.txt:160(搜「TMember」)
推断结果与悬停排障同上text/32-fm-generic-functions.txt:172(搜「firstNumber」) · text/32-fm-generic-functions.txt:188(搜「<number>(arr: number[]) => number」)
默认值与 unknown 兜底同上text/32-fm-generic-functions.txt:198(搜「T = string」) · text/32-fm-generic-functions.txt:214(搜「Set<unknown>」)
removeId 约束与 Omit 推断同上text/32-fm-generic-functions.txt:230(搜「does not exist on type」) · text/32-fm-generic-functions.txt:254(搜「Omit<」)
isAlbum 的难看签名同上text/32-fm-generic-functions.txt:289(搜「Record<」)
细检查破坏收窄同上text/32-fm-generic-functions.txt:324(搜「back to square one」)
类型谓词及危险评级同上text/32-fm-generic-functions.txt:337(搜「input is Album」) · text/32-fm-generic-functions.txt:364(搜「unsafe as」)
断言函数语法与激进收窄同上text/32-fm-generic-functions.txt:372(搜「asserts input is Album」) · text/32-fm-generic-functions.txt:387(搜「more aggressive」)
typeof null === 'object' 坑同上text/32-fm-generic-functions.txt:418(搜「typeof null === 'object'」)
重载签名与实现缺失报错同上text/32-fm-generic-functions.txt:422(搜「multiple function signatures」) · text/32-fm-generic-functions.txt:441(搜「implementation is missing」)
实现签名与混合调用报错同上text/32-fm-generic-functions.txt:447(搜「implementation signature」) · text/32-fm-generic-functions.txt:499(搜「No overload expects 2 arguments」)
重载 vs 并集同上text/32-fm-generic-functions.txt:507(搜「union type instead of function overloads」)
练习 16-1 createStringMap同上text/32-fm-generic-functions.txt:585(搜「Map<string, T>」)
练习 16-2 默认值覆盖同上text/32-fm-generic-functions.txt:614(搜「T = string」)
练习 16-3 uniqueArray 排查链同上text/32-fm-generic-functions.txt:696(搜「<unknown>」) · text/32-fm-generic-functions.txt:729(搜「arr: T[]」)
练习 16-4 addCodeToError同上text/32-fm-generic-functions.txt:814(搜「TError extends {message: string」)
练习 16-5 safeFunction 组合同上text/32-fm-generic-functions.txt:930(搜「Promise<() =>」) · text/32-fm-generic-functions.txt:936(搜「easier to read」)
练习 16-6 TArgs 双参数同上text/32-fm-generic-functions.txt:1024(搜「TArgs extends any[]」) · text/32-fm-generic-functions.txt:1028(搜「only thing that works」)
练习 16-7 assertIsAdminUser同上text/32-fm-generic-functions.txt:1109(搜「must return a value」) · text/32-fm-generic-functions.txt:1115(搜「asserts user is AdminUser」)
本章小结(中间地带)同上text/32-fm-generic-functions.txt:1140(搜「overlooked middle ground」)

Footnotes

  1. 出处:「Generic Functions」第 6–18 段(text/32-fm-generic-functions.txt:12,搜「third level」)。原文:「there's a third level that's often overlooked: the /utils folder」;groupBy、debounce、retry 及「might be used hundreds of times」见第 14 段。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. 出处:「Generic Functions」第 16 段(text/32-fm-generic-functions.txt:16,搜「save your team a lot of time」)。原文:抓住常见模式意味着代码「easier to maintain and faster to build」。 ↩

  3. 出处:「Generic Functions」第 141–156 段(text/32-fm-generic-functions.txt:152,搜「const first: any」)与第 156 段(搜「opt out」)。「so, how can you fix this? You need to make getFirstElement generic」见第 156 段。 ↩ ↩2

  4. 出处:「Generic Functions」第 158–164 段(text/32-fm-generic-functions.txt:160,搜「TMember」)。「it's common to prefix your type parameters with T」见第 164 段。 ↩

  5. 出处:「Generic Functions」第 166–177 段(text/32-fm-generic-functions.txt:172,搜「number | undefined」)。「The getFirstElement function is now type-safe」见第 177 段。 ↩

  6. 出处:「Generic Functions」第 179–192 段(text/32-fm-generic-functions.txt:188,搜「<number>(arr: number[]) => number」)。「try creating temporary function calls in the same file」见第 192 段。 ↩ ↩2

  7. 出处:「Generic Functions」第 28–38 段(text/32-fm-generic-functions.txt:34,搜「Expected 0 type arguments」)。「A function is generic if it declares a type parameter」见第 38 段。 ↩ ↩2 ↩3

  8. 出处:「Generic Functions」第 58–72 段(text/32-fm-generic-functions.txt:72,搜「position of the type parameter」)。 ↩ ↩2 ↩3

  9. 出处:「Generic Functions」第 76–85 段(text/32-fm-generic-functions.txt:83,搜「requires 1 type argument」)。 ↩ ↩2

  10. 出处:「Generic Functions」第 87–102 段(text/32-fm-generic-functions.txt:94,搜「infer it from the function」)。三个调用各自的推断结果见第 98–100 段(搜「result1」);「some of the best tools in TypeScript」见第 94 段。 ↩ ↩2 ↩3 ↩4 ↩5

  11. 出处:「Generic Functions」第 104–115 段(text/32-fm-generic-functions.txt:111,搜「not assignable to parameter of type」)。 ↩

  12. 出处:「Generic Functions」第 115 段(text/32-fm-generic-functions.txt:115,搜「source of truth」)。原文:「Passing type arguments is an instruction to TypeScript to override inference」。 ↩ ↩2 ↩3 ↩4

  13. 出处:「Generic Functions」第 194–207 段(text/32-fm-generic-functions.txt:198,搜「T = string」)。「The default doesn't impose a constraint on the type of T」见第 205 段。 ↩ ↩2 ↩3

  14. 出处:「Generic Functions」第 218–262 段(text/32-fm-generic-functions.txt:230,搜「does not exist on type」)。「Note how clever TypeScript is being here」见第 262 段;悬停结果 Omit 见第 254 段。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  15. 出处:「Generic Functions」第 934–936 段(text/32-fm-generic-functions.txt:936,搜「easier to read」)。 ↩ ↩2 ↩3 ↩4

  16. 出处:「Generic Functions」第 117–135 段(text/32-fm-generic-functions.txt:129,搜「Type parameter」)。「When you break generics down into these terms, it becomes much easier to understand」见第 135 段。 ↩ ↩2 ↩3 ↩4

  17. 出处:「Generic Functions」第 208–216 段(text/32-fm-generic-functions.txt:214,搜「Set<unknown>」)。 ↩ ↩2

  18. 出处:「Generic Functions」第 264–306 段(text/32-fm-generic-functions.txt:289,搜「Record<」)。「But it's not very helpful」见第 294 段;收窄后属性为 unknown 见第 296–305 段。 ↩

  19. 出处:「Generic Functions」第 307–333 段(text/32-fm-generic-functions.txt:324,搜「back to square one」)。原文:检查太复杂后 TypeScript「defaults to inferring boolean as the return type」。 ↩

  20. 出处:「Generic Functions」第 335–356 段(text/32-fm-generic-functions.txt:337,搜「input is Album」)。 ↩ ↩2

  21. 出处:「Generic Functions」第 358–364 段(text/32-fm-generic-functions.txt:364,搜「unsafe as」)。原文:「it's important to consider them about as unsafe as as and !」。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  22. 出处:「Generic Functions」第 366–396 段(text/32-fm-generic-functions.txt:372,搜「asserts input is Album」)。 ↩ ↩2 ↩3

  23. 出处:「Generic Functions」第 385–398 段(text/32-fm-generic-functions.txt:387,搜「more aggressive」)。 ↩ ↩2 ↩3

  24. 出处:「Generic Functions」第 400–418 段(text/32-fm-generic-functions.txt:418,搜「typeof null === 'object'」)。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  25. 出处:「Generic Functions」第 420–443 段(text/32-fm-generic-functions.txt:422,搜「multiple function signatures」)。 ↩ ↩2 ↩3

  26. 出处:「Generic Functions」第 503–521 段(text/32-fm-generic-functions.txt:507,搜「union type instead of function overloads」)。「This uses far fewer lines of code」见第 521 段。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  27. 出处:「Generic Functions」第 439–443 段(text/32-fm-generic-functions.txt:441,搜「implementation is missing」);实现签名的定义见第 445–447 段(搜「implementation signature」)。 ↩ ↩2 ↩3

  28. 出处:「Generic Functions」第 445–477 段(text/32-fm-generic-functions.txt:475,搜「combines the overloads」)。 ↩

  29. 出处:「Generic Functions」第 479–501 段(text/32-fm-generic-functions.txt:499,搜「No overload expects 2 arguments」)。 ↩ ↩2

  30. 出处:「Generic Functions」第 523–563 段(text/32-fm-generic-functions.txt:535,搜「createStringMap<number>」)。 ↩

  31. 出处:「Generic Functions」第 567–581 段(text/32-fm-generic-functions.txt:580,搜「Map<any, any>」)。 ↩

  32. 出处:「Generic Functions」第 582–595 段(text/32-fm-generic-functions.txt:585,搜「Map<string, T>」)。 ↩

  33. 出处:「Generic Functions」第 597–645 段(text/32-fm-generic-functions.txt:614,搜「T = string」)。 ↩

  34. 出处:「Generic Functions」第 647–681 段(text/32-fm-generic-functions.txt:662,搜「Equal<typeof result, number[]>>」)。 ↩

  35. 出处:「Generic Functions」第 683–721 段(text/32-fm-generic-functions.txt:696,搜「<unknown>」)。「there's no relationship between the things you're passing in and the thing you're getting out」见第 723 段。 ↩ ↩2 ↩3 ↩4 ↩5

  36. 出处:「Generic Functions」第 725–734 段(text/32-fm-generic-functions.txt:729,搜「arr: T[]」)。 ↩ ↩2

  37. 出处:「Generic Functions」第 746 段(text/32-fm-generic-functions.txt:746,搜「attempts to infer it」)。 ↩

  38. 出处:「Generic Functions」第 748–826 段(text/32-fm-generic-functions.txt:814,搜「TError extends {message: string」)。「does not restrict you to passing in only the properties defined in the constraint」见第 825 段。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  39. 出处:「Generic Functions」第 827–936 段(text/32-fm-generic-functions.txt:930,搜「Promise<() => Promise<number | Error>>」)。 ↩ ↩2

  40. 出处:「Generic Functions」第 1017–1028 段(text/32-fm-generic-functions.txt:1024,搜「TArgs extends any[]」)。「you might have tried this with unknown[], but any[] is the only thing that works」见第 1028 段。 ↩

  41. 出处:「Generic Functions」第 1030–1045 段(text/32-fm-generic-functions.txt:1036,搜「...args: TArgs」)。 ↩

  42. 出处:「Generic Functions」第 1115–1136 段(text/32-fm-generic-functions.txt:1115,搜「asserts user is AdminUser」)。 ↩ ↩2

  43. 出处:「Generic Functions」第 1093–1113 段(text/32-fm-generic-functions.txt:1109,搜「must return a value」)。 ↩

  44. 出处:「Generic Functions」第 1138–1148 段(text/32-fm-generic-functions.txt:1140,搜「overlooked middle ground」)。 ↩ ↩2

On this page

可复用的工具函数 — 泛型函数、类型谓词、断言函数与函数重载1. 第三层:/utils 文件夹,和它的复杂度预算2. 主线例子:getFirstElement,从 any 到泛型先看病灶:一个看似无害的工具函数修复:声明一个类型参数调用处发生了什么:推断推断错了怎么查:悬停函数名顺便立一条反模式:工具函数不许吃 any[]、吐 any3. 什么样的函数算泛型函数,以及三种写法4. 不传类型实参:推断;传了冲突:报错泛型类型与泛型函数的分水岭显式传实参 = 覆盖推断泛型类型 vs 泛型函数:一张对照表收束两章5. 术语正名:没有「一个泛型」这种东西6. 默认值与约束:和泛型类型同语法的两件事默认值:createSet 与 unknown 兜底约束:removeId 与「顺带学会 Omit」7. 类型谓词:让函数参与收窄需求:一个可复用的「它是不是 Album」第一个本能的修法,反而把收窄弄丢了正解:类型谓词(input is Album)危险声明:谓词和 as、! 一样不可信8. 断言函数:不返回 boolean,而是「不通过就抛」与谓词的同和异断言函数的专属坑:typeof null === "object"谓词还是断言函数:怎么选9. 函数重载:一副实现,多副签名需求:两种调用方式,一个函数实现签名:把重载收成一根并集参数重载 vs 并集:什么时候别用重载实现签名是「后台」,不是「门面」10. 七个练习:从「会用」到「会写」createStringMap:造一个泛型函数(练习 16-1)默认实参练习(16-2):T = stringuniqueArray:一次完整的推断排查(练习 16-3)addCodeToError:约束的实战(练习 16-4)safeFunction:泛型类型 × 泛型函数的组合(练习 16-5、16-6)assertIsAdminUser:断言函数收官(练习 16-7)练习收束:给工具函数写签名的五条纪律11. 这一章在全书链条中的位置12. 作者的判断与证据(分开标)13. 边界与局限14. 可带走的15. 原文地图Footnotes