追加参数 Append Argument
大约 3 分钟
追加参数 Append Argument
题目
实现一个泛型 AppendArgument<Fn, A>,对于给定的函数类型 Fn,以及一个任意类型 A,返回一个新的函数类型 G:G 与 Fn 的签名完全相同,但在最后额外追加了一个类型为 A 的参数。
type Fn = (a: number, b: string) => number
type Result = AppendArgument<Fn, boolean>
// 期望结果:(a: number, b: string, x: boolean) => number解答
type AppendArgument<Fn extends (...args: any[]) => any, A> =
Fn extends (...args: infer Args) => infer R
? (...args: [...Args, A]) => R
: never核心思路:用 infer 提取原函数的参数列表和返回类型,然后在参数末尾追加新类型,重新构造函数签名。
逐步拆解
第一步 — 约束 Fn 为可调用类型
Fn extends (...args: any[]) => any这里确保 Fn 必须是函数类型。没有这个约束,TypeScript 不允许对它进行函数类型推断。
第二步 — 用 infer 提取参数和返回类型
Fn extends (...args: infer Args) => infer RArgs捕获原始参数类型,以元组形式表示(如[number, string])R捕获返回类型(如number)
第三步 — 追加 A,重建函数签名
(...args: [...Args, A]) => R把 Args 展开并在末尾追加 A,构成新的参数元组。TypeScript 完全支持将元组类型用作 rest 参数。
推导过程:
Fn = (a: number, b: string) => number
A = boolean
Args = [number, string]
R = number
Result = (...args: [number, string, boolean]) => number
= (a: number, b: string, x: boolean) => number ✓深入分析
为什么不直接用 Parameters<> + ReturnType<>?
完全可以用内置工具类型来实现:
type AppendArgument<Fn extends (...args: any[]) => any, A> =
(...args: [...Parameters<Fn>, A]) => ReturnType<Fn>这种写法同样正确,可读性更强。条件类型 + infer 的写法是类型挑战里更常见的风格,但两种方式在实际项目中都可以用。
函数签名中的元组展开
(...args: [...Args, A]) 依赖 TypeScript 4.0 引入的可变元组类型(Variadic Tuple Types)。在 TS 4.0 之前,不支持将泛型元组展开到 rest 参数里——只能用函数重载或更复杂的方式绕过。
[...Args, A] 本质上是元组拼接:取出原始参数元组,在末尾加一个新类型。这与普通数组展开不同——TypeScript 会精确追踪每个元素的位置和类型。
如果 Fn 有可选参数或 rest 参数呢?
type Fn1 = (a?: number) => void
// Args = [number?]
// AppendArgument<Fn1, string> = (a?: number, x: string) => void
type Fn2 = (...args: number[]) => void
// Args = number[]
// AppendArgument<Fn2, string> = (...args: [...number[], string]) => voidinfer 的方式能自然处理这些情况——它直接捕获原始签名长什么样。
never 兜底
: never如果 Fn 不符合 (...args: infer Args) => infer R 的结构(有了约束之后理论上不会发生),就返回 never。属于防御性写法。
核心要点
infer Args作用在 rest 参数上,会把所有参数捕获为一个元组——非常方便后续操作[...Args, A]利用可变元组类型在已有参数列表末尾追加新类型- 条件推断版本和
Parameters<>+ReturnType<>版本都正确,选更清晰的那个 - 可变元组类型(TS 4.0+)是很多高级函数操作题目的基础,值得重点掌握
