#33823•Hard

OverloadedParameters

Collect the parameters of every overload of a function type into a union of tuples, padded with never so destructuring the result stays narrowable.

The built-in Parameters<T> sees only the last overload of a function. This challenge sees all of them. OverloadedParameters<F> returns a union with one tuple per call signature of F, every member padded to the same length. The payoff is in the implementation body of an overloaded function: destructure args once and then narrow, without ?. or !. guarding every access.

function func(fn: (arg: string) => string): string
 
function func(num: number, init: Set<string>): boolean
 
function func(...args: unknown[]): string | boolean {
  const [fnOrNum, init] = args as OverloadedParameters<typeof func>
 
  if (typeof fnOrNum === 'function') {
    return fnOrNum('foo')
  } else {
    return fnOrNum === init.size
  }
}

Challenge Instructions: OverloadedParameters

Hard

TypeScript's Parameters don't support overloaded functions, causing issues with type narrowing and undefined parameters during destructuring. The goal is to create utility types that extract parameters from all overloads, eliminating the need for ?. and !. after type narrowing.

function func(fn: (arg: string) => string): string
 
function func(num: number, init: Set<string>): boolean
 
function func(...args: unknown[]): string | boolean {
const [fnOrNum, init] = args as OverloadedParameters<typeof func>
 
if (typeof fnOrNum === 'function') {
return fnOrNum("foo")
} else {
return fnOrNum === init.size
}
}
 
func((s) => `Hello ${s}`)
func(1, new Set<string>('Hi'))

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

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

ChallengeSolution
/* _____________ Your Code Here _____________ */

type OverloadedParameters<F> = any

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

function func(fn: (args: string) => string): string

function func(num: number, init: Set<string>): boolean

function func(...args: unknown[]): string | boolean {
  const [fnOrNum, init] = args as OverloadedParameters<typeof func>

  if (typeof fnOrNum === 'function') {
    return fnOrNum('foo')
  } else {
    return fnOrNum === init.size
  }
}

func((s) => `Hello ${s}`)
func(1, new Set<string>('Hi'))

type cases = [

Pro Challenge

Get access to all 200+ challenges, including every medium, hard, and extreme one.

One-time payment, lifetime access. 30-day money-back guarantee.

Detailed Explanation

The solution in full:

type OverloadArgs<F> = F extends {
  (...args: infer A1): any
  (...args: infer A2): any
  (...args: infer A3): any
  (...args: infer A4): any
}
  ? A1 | A2 | A3 | A4
  : F extends {
        (...args: infer A1): any
        (...args: infer A2): any
        (...args: infer A3): any
      }
    ? A1 | A2 | A3
    : F extends { (...args: infer A1): any; (...args: infer A2): any }
      ? A1 | A2
      : F extends (...args: infer A1) => any
        ? A1
        : never
 
type DropFirst<Args> = Args extends [unknown, ...infer Rest] ? Rest : never
 
type MaxArity<Args, Acc extends unknown[] = []> = [DropFirst<Args>] extends [never]
  ? Acc['length']
  : MaxArity<DropFirst<Args>, [...Acc, unknown]>
 
type PadTo<Args extends unknown[], N extends number> = Args['length'] extends N
  ? Args
  : PadTo<[...Args, never], N>
 
type PadAll<Args, N extends number> = Args extends unknown[]
  ? PadTo<Args, N>
  : never
 
type OverloadedParameters<F> = PadAll<OverloadArgs<F>, MaxArity<OverloadArgs<F>>>

Three separate problems hide in there: reading every overload, finding the widest one, and widening the rest to match.

Reading every overload

A single infer against (...args: infer A) => any only ever captures the last call signature. TypeScript resolves an overloaded type against a single-signature target by picking the last one that fits, so the first overload is simply invisible. The way around it is to ask for a target that has as many call signatures as the source does. When the counts line up, TypeScript pairs them positionally and each infer captures its own overload. The overload count is not knowable in advance, so OverloadArgs tries the shapes from widest to narrowest and takes the first that matches: four signatures, then three, then two, then one.

type Two = typeof func extends { (...args: infer A1): any; (...args: infer A2): any }
  ? A1 | A2
  : never
// [fn: (args: string) => string] | [num: number, init: Set<string>]

A two-overload function falls through the first two branches and lands on the two-signature shape, which is why the cascade has to run downwards.

Finding the widest overload

The raw union has members of length 1 and 2. Destructuring it gives init the type Set<string> | undefined, which is exactly the !. problem the challenge is about, so every member has to be stretched to the longest arity. DropFirst peels one element off a naked type parameter, so it distributes across the union, and members that run out of elements become never and drop out:

// start:  [fn: F] | [num: number, init: Set<string>]
// peel 1: [] | [init: Set<string>]
// peel 2: []
// peel 3: never

MaxArity counts peels, but checks [DropFirst<Args>] extends [never] rather than [Args] extends [never]. That one-step lookahead is what makes the count come out at 2 instead of 3: the empty tuple left after peel 2 is still a live union member, and counting it would add a phantom parameter. The [...] brackets on both sides stop the check from distributing, so it asks whether the whole union is empty rather than testing member by member.

Padding the rest

PadTo appends never until the tuple reaches the target length, and PadAll distributes it over the union:

type Padded = PadAll<[fn: F] | [num: number, init: Set<string>], 2>
// [fn: F, never] | [num: number, init: Set<string>]

never is the right filler rather than undefined: destructuring the padded union gives init the type never | Set<string>, and never vanishes from a union, so init is plainly Set<string>. A filler of undefined would reintroduce the problem. One detail is easy to get wrong: MaxArity has to be evaluated before PadAll instantiates, which is why it sits in the argument position. Computing it inside the distributed conditional leaves the recursion in PadTo with an unresolved length to compare against, and the padding silently never happens.

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 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges