Technology · TypeScript

TypeScript Function Overloading

A reference for declaring multiple type signatures on a single TypeScript function.

TL;DR
  1. 01Declare multiple overload signatures before the implementation function.
  2. 02Implement the function body using union types to handle all signatures.
  3. 03Callers see only the matching overload, not the implementation signature.

Basic Function Overloading

  • Declare multiple overload signatures before the implementation.
    // Overload signatures
    function add(a: number, b: number): number;
    function add(a: string, b: string): string;
    
    // Implementation
    function add(a: any, b: any) {
      return a + b;
    }
    
    add(1, 2);     // OK: returns number
    add('a', 'b'); // OK: returns string
  • The implementation signature is not callable by consumers.
    function format(value: string): string;
    function format(value: number): string;
    
    function format(value: string | number): string {
      return String(value);
    }
    
    // format(true); // error — implementation signature is hidden
  • Use at least two overload signatures before the implementation.
    // One overload is not useful — use two or more
    function parse(input: string): number;
    function parse(input: number): string;
    
    function parse(input: string | number): string | number {
      return typeof input === "string" ? Number(input) : String(input);
    }
  • Overloads must be compatible with the implementation signature.
    function wrap(value: string): string[];
    function wrap(value: number): number[];
    
    function wrap(value: string | number): string[] | number[] {
      return [value as any];
    }
  • Use overloads to express arity differences explicitly.
    function createUser(name: string): User;
    function createUser(name: string, role: string): User;
    
    function createUser(name: string, role?: string): User {
      return { name, role: role ?? "user" };
    }

Optional Parameters

  • Use overloads to give different return types per argument count.
    function greet(name: string): string;
    function greet(name: string, age: number): string;
    
    function greet(name: string, age?: number) {
      if (age) {
        return `Hello, ${name}! You are ${age}.`;
      }
      return `Hello, ${name}!`;
    }
    
    greet('Alice');      // OK
    greet('Bob', 30);    // OK
  • Overloads prevent callers from passing invalid argument combinations.
    function connect(host: string): Connection;
    function connect(host: string, port: number): Connection;
    
    function connect(host: string, port?: number): Connection {
      return new Connection(host, port ?? 80);
    }
  • Optional overloads are clearer than one signature with many optional params.
    // Hard to reason about — too many optional flags
    // function render(a: string, b?: string, c?: string, d?: number): void
    
    // Better: separate overloads for each valid call pattern
    function render(template: string): void;
    function render(template: string, context: Record<string, string>): void;
    function render(template: string, context?: Record<string, string>): void {
      // implementation
    }
  • Name overload parameters consistently for readability.
    function fetch(url: string): Promise<Response>;
    function fetch(url: string, options: RequestInit): Promise<Response>;
    
    function fetch(url: string, options?: RequestInit): Promise<Response> {
      return window.fetch(url, options);
    }
  • Add a default-return overload last to handle the fallback case.
    function get(key: "count"): number;
    function get(key: "label"): string;
    function get(key: string): unknown;
    
    function get(key: string): unknown {
      return store[key];
    }

Different Return Types

  • Overloads let callers get the right return type per input type.
    function process(input: string): string;
    function process(input: number): number;
    
    function process(input: string | number): string | number {
      if (typeof input === 'string') {
        return input.toUpperCase();
      }
      return input * 2;
    }
    
    const str = process('hello'); // string
    const num = process(5);       // number
  • Without overloads, callers receive a union return type.
    // Without overloads — caller must narrow the return
    function convert(x: string | number): string | number {
      return typeof x === "string" ? x.toUpperCase() : x * 2;
    }
    
    const result = convert("hi"); // string | number — inconvenient
  • Map input literal types to different output types with overloads.
    function read(format: "json"): object;
    function read(format: "text"): string;
    function read(format: "buffer"): ArrayBuffer;
    
    function read(format: string): object | string | ArrayBuffer {
      // implementation chooses based on format
      return {};
    }
  • Use generics when the return type depends on the input type generically.
    // Overload approach
    function identity(x: string): string;
    function identity(x: number): number;
    function identity(x: any): any { return x; }
    
    // Generic approach — simpler for symmetric cases
    function identityG<T>(x: T): T { return x; }
  • Combine overloads with optional flags for conditional return shapes.
    function search(query: string, raw: true): string[];
    function search(query: string, raw?: false): SearchResult[];
    
    function search(query: string, raw?: boolean): string[] | SearchResult[] {
      const results = queryIndex(query);
      return raw ? results.map(r => r.raw) : results;
    }

Complex Overloads

  • Use overloads for functions that accept multiple input shapes.
    function merge(obj1: object, obj2: object): object;
    function merge(arr1: any[], arr2: any[]): any[];
    
    function merge(item1: any, item2: any) {
      if (Array.isArray(item1) && Array.isArray(item2)) {
        return [...item1, ...item2];
      }
      return { ...item1, ...item2 };
    }
  • Add overloads for callback vs non-callback variants of a function.
    function load(id: number): Promise<User>;
    function load(id: number, cb: (user: User) => void): void;
    
    function load(id: number, cb?: (user: User) => void): Promise<User> | void {
      const p = fetchUser(id);
      if (cb) { p.then(cb); } else { return p; }
    }
  • Overload class methods the same way as standalone functions.
    class Formatter {
      format(value: string): string;
      format(value: number): string;
      format(value: string | number): string {
        return String(value).trim();
      }
    }
  • Order overloads from most specific to least specific.
    function normalize(input: string[]): string[];
    function normalize(input: string): string;
    function normalize(input: string | string[]): string | string[] {
      return Array.isArray(input) ? input.map(s => s.trim()) : input.trim();
    }
  • Overload interface call signatures for typed function objects.
    interface Converter {
      (value: string): number;
      (value: number): string;
    }
    
    const convert: Converter = (value: any) =>
      typeof value === "string" ? Number(value) : String(value);

Best Practices

  • Keep overloads focused so each signature is clearly distinct.
    // Good: overloads are distinct
    function getValue(key: string): string;
    function getValue(key: number): number;
    
    // Avoid: overlapping overloads confuse callers
    // function getValue(key: string | number): any { }
  • Avoid overloads when a generic achieves the same result more simply.
    // Overloads: verbose for symmetric relationships
    function echo(x: string): string;
    function echo(x: number): number;
    
    // Generic: cleaner for identity-like functions
    function echo<T>(x: T): T { return x; }
  • Document each overload signature with JSDoc for IDE tooltips.
    /** Converts input to uppercase */
    function transform(input: string): string;
    /** Doubles a numeric input */
    function transform(input: number): number;
    function transform(input: string | number): string | number {
      return typeof input === "string" ? input.toUpperCase() : input * 2;
    }
  • Test every declared overload in your test suite.
    // Verify each overload signature independently
    const a: string = transform("hello"); // must be string
    const b: number = transform(5);       // must be number
  • Don't create an overload if a union parameter already expresses the intent.
    // Use union when both inputs produce the same output type
    function printValue(value: string | number): void {
      console.log(String(value));
    }

Tip: Use overloading when a function behaves differently based on input types — it's better than union types for callers.

Warning: Overloads are compile-time only — the implementation still needs to handle all types at runtime.

TypeScript Error MessagesTypeScript Generics Constraints