Function Overloads in TypeScript

Function overloads let a single function accept different argument counts and types. Learn overload signatures, the implementation signature, and when to use a union instead.

6 min read

TypeScript function overloads describe multiple distinct ways a function can be called. You write one or more overload signatures above the function, then provide a single implementation that handles all of them. Overloads are useful when different argument counts produce different return types, which a simple union cannot express.

Overload resolution flow

TypeScript tries each overload signature in order. The first one that matches the arguments is used. If none match, the call is rejected. The implementation signature is never tried.

Overload Signatures and the Implementation

An overloaded function has two parts: the overload signatures that callers see, and the implementation signature that only the function body uses. Here is the classic example:

typescripttypescript
// Overload signatures: what callers can use
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
 
// Implementation signature: handles all cases internally
function makeDate(timestampOrYear: number, month?: number, day?: number): Date {
  if (month !== undefined && day !== undefined) {
    return new Date(timestampOrYear, month, day);
  }
  return new Date(timestampOrYear);
}

Calling this function with either one argument or three arguments matches one of the two overload signatures, and both calls return a valid Date:

typescripttypescript
const fromTimestamp = makeDate(1700000000000);
const fromParts = makeDate(2026, 7, 17);
 
console.log(fromTimestamp.getFullYear());
console.log(fromParts.getFullYear());
// Output: 2023
// Output: 2026

The two overload signatures declare that this function accepts either one argument or three arguments. The implementation signature uses optional parameters to handle both cases in a single body.

The key rule: the implementation signature is not callable. Even though it has optional parameters, you cannot call makeDate with two arguments:

typescripttypescript
const invalid = makeDate(2026, 7);

Neither overload signature accepts exactly two arguments, so the compiler rejects the call even though the implementation signature technically has room for it:

texttext
No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

Only the overload signatures determine what callers can do. The implementation signature is an internal detail.

When Overloads Are Useful

Overloads shine when different argument counts produce different return types. A union parameter cannot express this:

typescripttypescript
const settings: Record<string, string> = { theme: "dark" };
 
// Overloads: return type depends on the argument
function getValue(key: string): string | undefined;
function getValue(key: string, defaultValue: string): string;
 
function getValue(key: string, defaultValue?: string): string | undefined {
  const value = settings[key];
  if (value !== undefined) return value;
  return defaultValue;
}

Calling this function with one argument keeps the possibly-missing type, while calling it with a default argument narrows the return type to a guaranteed string:

typescripttypescript
const a = getValue("missing");
// Type: string | undefined
 
const b = getValue("missing", "Guest");
// Type: string, never undefined
 
console.log(a?.toUpperCase());
console.log(b.toUpperCase());
// Output: undefined
// Output: GUEST

The first overload returns string | undefined because the caller did not provide a default. The second overload returns a plain string only because the default guarantees a value. A single implementation with a union return type would lose this precision: every caller would see the possibly-undefined type regardless of whether they passed a default.

When to Use a Union Instead

Overloads have a limitation: TypeScript resolves a call to exactly one overload. A union argument that could match multiple overloads will fail even when the overloads logically cover it:

typescripttypescript
// Overloads for string and array
function len(value: string): number;
function len(value: unknown[]): number;
function len(value: string | unknown[]): number {
  return value.length;
}
 
len("hello");   // OK
len([1, 2, 3]); // OK
len(Math.random() > 0.5 ? "hello" : [1, 2, 3]);

The conditional expression could be either a string or an array, and that combined union type does not match either overload signature on its own, so the compiler rejects the call with both overloads listed as failures:

texttext
No overload matches this call.
  Overload 1 of 2, '(value: string): number', gave the following error.
    Argument of type '"hello" | number[]' is not assignable to parameter of type 'string'.
      Type 'number[]' is not assignable to type 'string'.
  Overload 2 of 2, '(value: unknown[]): number', gave the following error.
    Argument of type '"hello" | number[]' is not assignable to parameter of type 'unknown[]'.
      Type 'string' is not assignable to type 'unknown[]'.

The expression Math.random() > 0.5 ? "hello" : [1, 2, 3] has the type string | number[]. This union does not match either individual overload. But a simple union parameter handles it correctly:

typescripttypescript
// Union parameter: works for all cases
function len(value: string | unknown[]): number {
  return value.length;
}
 
len("hello");   // OK
len([1, 2, 3]); // OK
len(Math.random() > 0.5 ? "hello" : [1, 2, 3]); // OK

The rule from the TypeScript handbook: always prefer parameters with union types instead of overloads when possible. Use overloads only when the return type must change based on the argument count or when the relationship between argument types cannot be expressed as a union.

The Implementation Must Be Compatible

The implementation signature must accept all the argument combinations that the overload signatures declare. If the implementation is more restrictive than the overloads, TypeScript reports an error:

typescripttypescript
// Overload expects string
function format(value: string): string;
 
// Implementation only accepts number
function format(value: number): string {
  return value.toString();
}

The implementation only accepts numbers, but callers are promised they can pass a string, so TypeScript refuses to accept the mismatched pair:

texttext
This overload signature is not compatible with its implementation signature.

The overload says callers can pass a string, but the implementation only handles number. The implementation must be at least as permissive as all overloads combined, the same compatibility rule covered in type function parameters in TypeScript.

A safe pattern: make the implementation parameters a union of all overload parameter types, and use optional parameters for arguments that not every overload provides:

typescripttypescript
function describe(value: string): string;
function describe(value: string, detailed: boolean): string;
 
// Implementation matches both overloads
function describe(value: string, detailed?: boolean): string {
  if (detailed) {
    return `Detailed: ${value}`;
  }
  return value;
}

The implementation uses detailed?: boolean to cover both the one-argument and two-argument overloads.

Overloads Are Not for Callbacks

Do not use overloads to type a callback parameter. Callbacks already benefit from the parameter count compatibility rule: a callback with fewer parameters is always valid. If you find yourself writing overloads for a callback, simplify it to a union type or a single signature with optional parameters only when you genuinely sometimes skip arguments.

For more on this, see type callback functions in TypeScript.

Common Mistakes

Trying to call the implementation signature directly. Only overload signatures determine valid calls. If callers need a specific argument pattern, add an overload signature for it.

Using overloads when the return type is the same. If every overload returns the same type, replace them with a single signature using union parameters. The code is shorter and callers with union arguments work correctly.

Writing overloads that differ only by parameter type, not count. If the only difference is the type of a single parameter, use a union type on that parameter. Overloads for string | number are almost never the right approach.

Adding too many overloads. Two or three overloads are typical. If a function needs more than four, reconsider the design. The function may be doing too many things, or a different type-level approach like generics may be cleaner.

Rune AI

Rune AI

Key Insights

  • Overload signatures describe each valid way to call the function.
  • The implementation signature handles all cases but is never callable from outside.
  • The implementation must be compatible with all overload signatures.
  • Prefer union parameters over overloads when the return type is the same.
  • Use overloads when argument count changes the return type.
  • Never use overloads for callbacks: declare all parameters as required instead.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between overload signatures and the implementation signature?

Overload signatures are the callable signatures visible to callers. The implementation signature is only used to write the function body and is not visible from the outside. It must be compatible with all overload signatures combined.

When should I use overloads instead of union types?

Use overloads when different argument counts produce different return types, or when the relationship between argument types and return types cannot be expressed with a union. For the same return type across different input types, prefer a union parameter.

Can the implementation signature be called directly?

No. Only the overload signatures are callable. The implementation signature is an internal detail. This is why makeDate with two arguments fails even though the implementation signature accepts two optional parameters.

Conclusion

Function overloads let a function accept different argument counts and types while maintaining precise return types. Write overload signatures above the implementation, one per valid call pattern. The implementation signature handles all cases internally but is never callable. Overloads are a specialized tool: when the return type is the same across all calls, use a union parameter instead. When argument count drives the return type, overloads are the right choice.