Yuzhe's Blog

yuzhes

Append Argument

Append Argument

Challenge Link

Challenge

For given function type Fn, and any type A (any in this context means we don’t restrict the type), create a generic type which will take Fn as the first argument, A as the second, and will produce function type G which will be the same as Fn but with appended argument A as a last one.

For example:

type Fn = (a: number, b: string) => number

type Result = AppendArgument<Fn, boolean>
// expected: (a: number, b: string, x: boolean) => number

Solution

type AppendArgument<Fn extends (...args: any[]) => any, A> =
  Fn extends (...args: infer Args) => infer R
    ? (...args: [...Args, A]) => R
    : never

The key idea: infer the original parameters and return type, then rebuild the function signature with the new argument appended at the end.

Breaking it down

Step 1 — Constrain Fn to a callable

Fn extends (...args: any[]) => any

This ensures Fn is a function type. Without this constraint, TypeScript won’t let us perform function-type inference on it.

Step 2 — Extract params and return type via infer

Fn extends (...args: infer Args) => infer R

Step 3 — Rebuild with A appended

(...args: [...Args, A]) => R

Spreading Args and appending A into a new tuple gives us the extended parameter list. TypeScript happily accepts this as a valid rest parameter.

Walkthrough:

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  ✓

Deep Dive

Why use infer instead of Parameters<> + ReturnType<>?

You could write it using built-in utilities:

type AppendArgument<Fn extends (...args: any[]) => any, A> =
  (...args: [...Parameters<Fn>, A]) => ReturnType<Fn>

This works fine and is arguably more readable. The conditional infer version is more idiomatic for challenges but both are valid in practice.

Tuple spread in function signatures

(...args: [...Args, A]) relies on variadic tuple types introduced in TypeScript 4.0. Before TS 4.0, spreading a generic tuple into rest parameters wasn’t supported — you’d have to use overloads or more complex workarounds.

The spread [...Args, A] is a tuple concatenation: it takes the original params tuple and appends one more element. This is distinct from array spread — TypeScript tracks the exact position and type of each element.

What if Fn has optional or rest parameters?

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

The infer approach handles these cases naturally — it captures whatever the original signature looks like.

never fallback

: never

If Fn somehow doesn’t match (...args: infer Args) => infer R (which can’t happen given the constraint), we fall through to never. It’s a defensive catch-all.

Key Takeaways