infer 与函数类型提取
从结构中提取属性、参数列表与返回类型,并理解位置和使用边界。
本章目录CONTENTS ↓
一个函数已经定义了输入和输出,表单、测试数据或其他调用方却还要手写一遍相同的类型。签名发生变化时,这些副本容易不同步。本章从这个维护问题出发,解释如何从类型结构中提取信息;完整的测试条目设计见关联实战。
为什么需要从结构中提取类型
假设创建用户的函数类型已经存在,另一个模块需要复用它返回的用户结构。
type CreateUser = () => { id: string; name: string };
type User = ReturnType<CreateUser>;
// { id: string; name: string }
type Id = User["id"]; // string对象属性可以通过 User["id"] 提取。函数的返回类型却不是一个名叫 return 的属性,不能靠 CreateUser["return"] 访问。这里需要描述函数结构,再提取返回位置上的类型;内置的 ReturnType 已经提供了这种工具。
infer 支持在条件类型匹配结构时,声明一个待推导的类型变量,供匹配成功后的分支使用。它让自定义提取规则成为可能,也帮助我们理解内置工具的机制。
什么时候使用,什么时候直接复用
| 需求 | 选择 |
|---|---|
| 已有独立导出的业务类型 | 直接引用,不必从函数反推 |
| 读取已知对象的属性类型 | 索引访问 T["属性名"] |
| 取得函数返回类型或参数列表 | 优先使用 ReturnType 或 Parameters |
| 按结构提取并组织成新的业务类型 | 条件类型配合 infer |
提取适合让类型跟随已有签名变化。如果业务契约应该独立于具体实现,单独定义并复用业务类型仍然合适。这里的类型计算不会调用函数,也不会修改运行时对象。
infer 写在哪里,与泛型有什么关系
先看一个没有泛型参数的例子,只关注语法位置:
type Payload =
{ payload: string } extends { payload: infer P }
? P
: never;
// stringinfer P 写在条件类型 extends 右侧的结构里。左边匹配这个结构时,payload 位置对应 string,所以 P 推导为 string,真分支返回它。P 是局部的类型变量名称,不是 JavaScript 变量。
不需要先声明一个外层泛型才能使用 infer。通常写成泛型工具,是为了对不同输入重复使用这段提取逻辑。infer 声明只能放在条件类型的匹配位置,不能直接写在普通泛型参数的约束中;推导出的类型变量可以在该条件的真分支使用。
从返回位置提取:复用函数的结果
先只处理能无参调用的普通函数,避免一开始把所有参数形状混在一起。
type GetResult<T> = T extends () => infer R ? R : never;
type LoadNames = () => string[];
type Names = GetResult<LoadNames>; // string[]T 是整个 () => string[];R 是返回位置上的 string[]。目标是复用返回结果,所以真分支写 R。如果写 T,仍然得到整个函数类型。
这份入门工具不适合需要必填实参的函数;实际取得普通函数的返回类型可以直接使用内置 ReturnType。重载、泛型函数等推导边界不在本章范围内。
从参数位置提取:复用事件数据
如果只拿到了一个事件处理器签名,测试数据需要跟随它接收的事件类型。把 infer 放在参数位置,就能提取这一部分。
type EventParam<T> =
T extends (e: infer Event) => unknown ? Event : never;
type SavedHandler =
(e: { kind: "saved"; id: string }) => void;
type FailedHandler =
(e: { kind: "failed"; error: string }) => void;
type SavedEvent = EventParam<SavedHandler>;
// { kind: "saved"; id: string }
type Events = EventParam<SavedHandler | FailedHandler>;
// { kind: "saved"; id: string }
// | { kind: "failed"; error: string }这里 e 是参数名,Event 是待推导的类型名称;返回位置的 unknown 不筛选具体返回类型。该例先限定为单个必填参数的普通函数。
对于处理器联合,条件左侧是裸类型参数 T,因此逐成员处理,再把各自提取的结果组成联合。分配由条件左侧的形状决定,提取的位置由 infer 决定。 如果项目已经导出了事件类型,直接引用通常更简单。
多个参数为什么需要元组
准备一次函数调用的测试数据时,只知道“参数可能是字符串或数字”还不够,还要知道它们的顺序和数量。两个必填参数可以用元组描述。
type UpdateArgs = [id: string, count: number];
const updateArgs: UpdateArgs = ["item-1", 3];
type F1 = (id: string, count: number) => void;
type F2 = (...args: [string, number]) => void;[string, number] 要求两个位置:第一项是字符串,第二项是数字。(string | number)[] 则允许任意长度与任意位置的两种类型,不能准确描述这个调用要求。元组中的标签便于阅读,不改变这里各位置的类型约束。
F1 和 F2 表达相同的调用参数要求。...args 代表整个参数列表,元组类型描述列表的各个位置。于是,可以用 infer 推导这个列表。
type ArgsOf<T> =
T extends (...args: infer P) => unknown ? P : never;
type Search = (query: string, limit: number) => boolean;
type SearchArgs = ArgsOf<Search>;
// [query: string, limit: number]P 是完整参数列表的类型,不是所有参数值类型的联合。一般业务直接使用 Parameters<Search> 即可;自定义工具适合继续组合规则。
只需要第一个参数时,再做索引访问
表单可能只需要保存函数的业务数据,不需要后面的重试选项。先提取参数元组,再索引第一项,就可以复用数据类型。
type FirstArg<T> =
T extends (...args: infer P) => unknown ? P[0] : never;
type Save = (
data: { id: string; title: string }, retry: boolean
) => void;
type SaveData = FirstArg<Save>;
// { id: string; title: string }这里的 P[0] 与对象属性索引一样,是类型层面的访问。若已有参数元组,直接索引即可;一次性提取也可以写 Parameters<Save>[0]。本节使用至少一个必填参数的普通函数,空参数与可选参数下如何设计“第一项”需要另外约定。
同时提取多个位置的信息
当一个新类型需要同时描述输入和输出时,可以在同一结构中声明两个待推导变量。下面只演示提取规则;如何组织成测试数据并保留联合配对,见 API 测试实战。
type FunctionParts<T> = T extends (...args: infer P) => infer R
? { parameters: P; result: R } : never;
type Parts = FunctionParts<(id: string) => number>;
// { parameters: [id: string]; result: number }
type EmptyParameters = ArgsOf<() => number>; // []
type NonFunction = ArgsOf<string>; // never空参数列表表示零个参数,函数匹配仍然成功。非函数不匹配结构,才走假分支。这里只讨论自定义工具;内置 Parameters 对非函数实参有约束检查。
官方阅读
- 条件类型中的 infer:结构提取与真分支使用。
- infer 的声明位置:条件匹配与普通泛型约束的区别。
- Parameters 与 ReturnType:已有工具的用途。
- 元组类型的剩余参数:参数列表与元组的对应。
- 映射类型 与 索引访问:逐键构造与取值。
这一页,先记到这里。