#38133Medium

OverwriteArg

Swap the type of a function's Nth argument while every other parameter and the return type stay untouched. Infer the parameter tuple, then map over it by index.

OverwriteArg<F, N, T> takes a function type, an argument position and a replacement type, and gives back the same signature with exactly one parameter swapped. Everything else stays where it was: the other parameters keep their types, the return type keeps its type, and the arity does not change.

It is a small surgical edit on a function type, which is what you need when you wrap a third-party callback and want to hand the caller a richer value in one slot. The interesting part is that a function's parameter list is a tuple under the hood, so once you get hold of that tuple you can treat this as an indexed tuple edit.

type F = (a: string, b: number, c: boolean) => void
 
type R1 = OverwriteArg<F, 0, Date>
// => (a: Date, b: number, c: boolean) => void
 
type R2 = OverwriteArg<F, 1, string[]>
// => (a: string, b: string[], c: boolean) => void
 
type R3 = OverwriteArg<F, 2, null>
// => (a: string, b: number, c: null) => void

Challenge Instructions: OverwriteArg

Medium

Implement a generic type OverwriteArg<F, N, T> that replaces the type of the Nth argument (0-indexed) of a function F with type T, while keeping all other argument types and the return type unchanged.

type F = (a: string, b: number, c: boolean) => void
 
type R1 = OverwriteArg<F, 0, Date>
// => (a: Date, b: number, c: boolean) => void
 
type R2 = OverwriteArg<F, 1, string[]>
// => (a: string, b: string[], c: boolean) => void
 
type R3 = OverwriteArg<F, 2, null>
// => (a: string, b: number, c: null) => void

View on GitHub: https://tsch.js.org/38133

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

type OverwriteArg<F extends (...args: any[]) => any, N extends number, T> = any

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

type F1 = (a: string, b: number, c: boolean) => void
type F2 = (x: Date) => string

type cases = [
  // Replace 0th argument
  Expect<Equal<
    OverwriteArg<F1, 0, Date>,
    (a: Date, b: number, c: boolean) => void
  >>,
  // Replace 1st argument
  Expect<Equal<
    OverwriteArg<F1, 1, string[]>,
    (a: string, b: string[], c: boolean) => void
  >>,
  // Replace 2nd arg

Pro Challenge

Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

The solution in full:

type OverwriteArg<F extends (...args: any[]) => any, N extends number, T> =
  F extends (...args: infer Args) => infer R
    ? (...args: { [I in keyof Args]: I extends `${N}` ? T : Args[I] }) => R
    : never

Three moves: pull the function apart, rewrite one slot of the parameter tuple, put the function back together.

Taking the function apart

A conditional type with infer is the standard way to look inside a function type. The pattern (...args: infer Args) => infer R matches any function and binds both halves at once:

type F1 = (a: string, b: number, c: boolean) => void
// Args = [a: string, b: number, c: boolean]
// R    = void

Args comes back as a tuple, with the parameter names preserved as labels. Those labels are documentation only, so [a: string] and [x: string] are the same type as far as TypeScript is concerned. That is why the tests can compare against (a: Date, ...) even though your solution never sees the letter a.

You could reach for the built-in Parameters<F> and ReturnType<F> instead. They are implemented with the same conditional, and one infer pattern that binds both is less noisy than two utility calls.

Rewriting one slot of the tuple

Mapping over a tuple works differently from mapping over an object. When the source of [I in keyof X] is the type parameter itself and X is a tuple or array, TypeScript keeps the shape: you get a tuple of the same length back, not an object with length and map keys on it.

type Identity = { [I in keyof Args]: Args[I] }
// [a: string, b: number, c: boolean], unchanged

So the mapped type is a per-position rewrite. I runs over the element positions, and whatever you put on the right becomes the new type at that position. Replace the value for one I and leave Args[I] everywhere else, and you have the edit:

type Replaced = { [I in keyof Args]: I extends '1' ? string[] : Args[I] }
// [a: string, b: string[], c: boolean]

Comparing the index

One detail decides whether this works. Inside a mapped type over a tuple, I is bound to the string index: '0', '1', '2', not the numbers 0, 1, 2. N, on the other hand, arrives as a numeric literal, so I extends N is false at every position and nothing is ever replaced.

A template literal type is the shortest bridge. Wrapping N in `${N}` converts the numeric literal to its string form:

// `${0}` is '0'
// '0' extends '0' → true, so position 0 gets T
// '1' extends '0' → false, so position 1 keeps Args[1]

Template literal types do this conversion for numbers, strings, booleans and bigint, which makes them a handy way to compare values that live in different literal worlds.

Putting the function back together

(...args: SomeTuple) => R builds a function type from a parameter tuple. Give it the mapped tuple and the inferred return type, and you have the answer. The : never branch is unreachable in practice because the constraint F extends (...args: any[]) => any already rejects non-functions, but a conditional type needs a false branch and never is the honest one.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges