Function Overloads in TypeScript

October 16, 2026 · 3 min read

Some functions return different things depending on what you pass in:

parse('2026-10-16')       // one Date
parse(['2026-10-16', …])  // an array of Dates

With a single union signature, every caller gets the union back:

function parse(input: string | number | string[]): Date | Date[]

const d = parse('2026-10-16')
d.getFullYear()   // error: Property 'getFullYear' does not exist on type 'Date[]'

You know it's a Date; the compiler doesn't. Overloads let you list each pairing of argument and return type separately.

Writing overloads

List the signatures callers may use, then write one implementation whose signature is compatible with all of them:

function parse(input: string): Date
function parse(input: number): Date
function parse(input: string[]): Date[]
function parse(input: string | number | string[]): Date | Date[] {
  if (Array.isArray(input)) return input.map((s) => new Date(s))
  return new Date(input)
}

The first three lines are the overload signatures: they have no body, and they're what callers see. The last is the implementation signature, which callers can't use directly — it's there to type-check the body.

function parse(input: string): Date
function parse(input: number): Date
function parse(input: string[]): Date[]
function parse(input: string | number | string[]): Date | Date[] {
if (Array.isArray(input)) return input.map((s) => new Date(s))
return new Date(input)
}
 
const a = parse('2026-10-16') // ?
const b = parse(['2026-10-16']) // ?
const c = parse(true) // ?
declare const id: string | number
const d = parse(id) // ?
what callers see
signatures3 overloads
implementation signaturehidden from callers

Overloads are several call signatures for one function, followed by one implementation. Callers only see the overload list; the implementation signature must be compatible with all of them but can't be called directly.

0 / 4

The rules

Order matters. TypeScript picks the first overload that fits, top to bottom. Put specific signatures before general ones. If (input: unknown): unknown came first, it would match everything and the others would never be used.

The implementation must cover every overload. Each overload's parameters must be assignable to the implementation's, and the implementation's return type must be compatible with each overload's. The compiler checks this, but it can't check that the body actually returns a Date[] for arrays — that's on you. Overloads are a promise you make to callers.

Unions don't spread across overloads. As the last step above shows, a string | number argument matches neither the string overload nor the number one. If callers will pass unions, add an overload for the union itself:

function parse(input: string | number): Date

Often you don't need overloads

When only the return type varies with an input type, a generic may do:

function first<T>(items: T[]): T | undefined {
  return items[0]
}

When the mapping is a rule, a conditional type expresses it once:

type Parsed<T> = T extends string[] ? Date[] : Date

function parse<T extends string | number | string[]>(input: T): Parsed<T> {
  return (Array.isArray(input) ? input.map((s) => new Date(s)) : new Date(input)) as Parsed<T>
}

This also handles unions: Parsed<string | string[]> distributes to Date | Date[]. The cost is a cast in the body, because TypeScript can't narrow a generic T inside the implementation. See conditional types.

When options change behaviour, separate functions are clearer. parseOne and parseMany are easier to read, document and search for than one function with three personalities.

Overloads are the right tool when the signatures are genuinely different shapes — different numbers of arguments, or unrelated types — and the set is small and fixed.

Overloads you already use

The DOM's type definitions use overloads heavily:

document.createElement('canvas')    // HTMLCanvasElement
document.createElement('input')     // HTMLInputElement

That's an overload over a mapped type: createElement<K extends keyof HTMLElementTagNameMap>(tagName: K): HTMLElementTagNameMap[K], plus a fallback returning HTMLElement for any other string. addEventListener works the same way, so the event in a 'keydown' handler is typed as a KeyboardEvent.

In classes and interfaces, overloads are written as several method signatures:

interface Cache {
  get(key: string): string | undefined
  get(key: string, fallback: string): string
}

Here the second overload promises that passing a fallback means you always get a string back.