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) => voidImplement 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) => voidView on GitHub: https://tsch.js.org/38133
Change the following code to make the test cases pass (no type check errors).
/* _____________ 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 argUnlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
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
: neverThree moves: pull the function apart, rewrite one slot of the parameter tuple, put the function back together.
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 = voidArgs 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.
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], unchangedSo 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]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.
(...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.
OverwriteArg<F1, 0, never> produces (a: never, b: number, c: boolean) => void. The mapped type writes never into position 0 without collapsing the tuple, because never inside a tuple is just an element type. A function that can never be called is a valid type.OverwriteArg<F2, 0, number> on the single-argument (x: Date) => string checks that the return type is carried over untouched. A solution built on Parameters<F> alone, forgetting ReturnType<F>, fails here.N = 2 on a three-parameter function) confirms that the other positions are copied rather than dropped, which is the usual failure mode of recursive attempts that rebuild the tuple by hand.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges