Yuzhe's Blog

yuzhes

追加参数 Append Argument

追加参数 Append Argument

题目链接

题目

实现一个泛型 AppendArgument<Fn, A>,对于给定的函数类型 Fn,以及一个任意类型 A,返回一个新的函数类型 GGFn 的签名完全相同,但在最后额外追加了一个类型为 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 R

第三步 — 追加 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]) => void

infer 的方式能自然处理这些情况——它直接捕获原始签名长什么样。

never 兜底

: never

如果 Fn 不符合 (...args: infer Args) => infer R 的结构(有了约束之后理论上不会发生),就返回 never。属于防御性写法。

核心要点