C.W.K.
Stream
Lesson 02 of 05 · published

Function Overloads: One Name, Many Signatures

~11 min · functions, overloads, one-name-many-signatures

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Overloads are for when one function is really several functions wearing the same hat."

The shape of an overload

A function overload is a way to give a single function multiple call signatures. You write the overload signatures first (the public contract), then a single implementation signature that accepts a union of everything. The implementation signature is NOT directly callable — only the overload signatures are.

function format(value: number): string;
function format(value: Date): string;
function format(value: number | Date): string {
  // implementation handles both
  return value instanceof Date ? value.toISOString() : value.toFixed(2);
}

Callers see two distinct signatures: pass a number or a Date, get a string. The compiler routes the call to the implementation. The implementation does runtime checks to handle both inputs.

When overloads are the right tool

Use overloads when the function genuinely has multiple, distinct call shapes AND a single signature (with union types, generics, or conditional types) doesn't express the relationship cleanly.

Counter-examples — places where a single signature wins:

  • Same parameter, multiple types: function format(value: number | Date): string — union, no overload needed.
  • Type-dependent return: function wrap<T>(x: T): { value: T } — generic, no overload needed.
  • Conditional return based on input: function f<T>(x: T): T extends string ? number : string — conditional type, no overload needed.

Where overloads do shine: when call shapes have different parameter counts, or when distinct call shapes need distinct documentation in IDE hover.

The overload-vs-union rule: If your function's parameter and return types are linked across a single signature, use that signature (with union, generic, or conditional types). If they are not linked — e.g., "call form A or call form B, no shared types" — use overloads.

The implementation-signature pitfall

The implementation signature is private — callers can't see it. But it must accept the union of all overload signatures. If you write four overloads but a narrow implementation, the compiler complains.

Worse: the implementation signature is checked for compatibility but is NOT part of the public type. Even if it accepts any, callers can only use the public signatures. This catches developers who assume "my implementation takes any, so this call works."

Pippa's confession

cwkPippa has maybe three overloaded functions in the entire frontend. Two of them could have been generics. One of them genuinely needed overloads — an API caller where two different call shapes (with-id-only vs with-id-and-filter) returned different result shapes. The lesson: overloads are real tools, but most attempts at them turn out to want generics. Try generics first.

Code

Overload — when the return depends on which form you call·typescript
// A genuine overload — different shapes, different return types.

function findUser(id: number): User;
function findUser(query: { email: string }): User[];
function findUser(arg: number | { email: string }): User | User[] {
  if (typeof arg === 'number') {
    return getUserById(arg);             // returns a single User
  } else {
    return searchUsersByEmail(arg.email); // returns User[]
  }
}

const u: User = findUser(1);             // ✅ picks the first overload
const us: User[] = findUser({ email: 'a@b.c' });  // ✅ picks the second

// The implementation signature isn't directly callable.
// findUser('hello');                    // ❌ no matching overload
Most 'overload-shaped' problems want generics or conditional types·typescript
// Counter-example: union + generic is usually enough.

// Not overload-worthy — just union the param.
function format(v: number | Date): string {
  return v instanceof Date ? v.toISOString() : v.toFixed(2);
}

// Not overload-worthy — generic + conditional.
type WrapReturn<T> = T extends string ? { kind: 'str'; value: T } : { kind: 'num'; value: T };
function wrap<T extends string | number>(x: T): WrapReturn<T> {
  return (typeof x === 'string' ? { kind: 'str', value: x } : { kind: 'num', value: x }) as WrapReturn<T>;
}

const a = wrap('hi');   // a: { kind: 'str'; value: 'hi' }
const b = wrap(42);     // b: { kind: 'num'; value: 42 }

External links

Exercise

Write an overloaded parse function: parse(input: string): string[] (split on comma) and parse(input: string, json: true): unknown (JSON.parse). The implementation handles both. Then rewrite it as a single function with a generic and a conditional return type. Which version reads cleaner? Which is easier to extend?
Hint
The overload version is more explicit and reads like documentation. The generic version is more compositional and scales better as you add new return shapes. The right answer depends on the project — but most teams settle on one default and use the other only when the default doesn't fit.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.