Hack Frontend Community

Function Overloads in TypeScript

What are Function Overloads?

Function Overloads are the ability to declare multiple signatures for a single function. TypeScript will choose the correct signature based on the passed arguments.


Basic Syntax

// Overload signatures
function greet(name: string): string;
function greet(firstName: string, lastName: string): string;

// Implementation signature
function greet(firstName: string, lastName?: string): string {
  if (lastName) {
    return `Hello, ${firstName} ${lastName}!`;
  }
  return `Hello, ${firstName}!`;
}

// Usage
greet('John');              // "Hello, John!"
greet('John', 'Doe');       // "Hello, John Doe!"
// greet('John', 'Doe', 'Jr.');  // Error: Expected 1-2 arguments

Why Use Overloads?

Different Return Types

function getValue(key: 'name'): string;
function getValue(key: 'age'): number;
function getValue(key: 'active'): boolean;
function getValue(key: string): string | number | boolean {
  const data = {
    name: 'John',
    age: 30,
    active: true
  };
  return data[key as keyof typeof data];
}

const name = getValue('name');      // string
const age = getValue('age');        // number
const active = getValue('active');  // boolean
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: 'span'): HTMLSpanElement;
function createElement(tag: 'button'): HTMLButtonElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const div = createElement('div');      // HTMLDivElement
const span = createElement('span');    // HTMLSpanElement
const button = createElement('button'); // HTMLButtonElement

Optional Parameters with Different Types

function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
  if (month !== undefined && day !== undefined) {
    return new Date(yearOrTimestamp, month, day);
  }
  return new Date(yearOrTimestamp);
}

const date1 = makeDate(1234567890);     // Date
const date2 = makeDate(2024, 11, 19);   // Date
// const date3 = makeDate(2024, 11);    // Error

Signature Order Matters

TypeScript checks signatures top-to-bottom and uses the first matching one.

// Wrong - general signature first
function process(value: any): any;
function process(value: string): string;
function process(value: number): number;
function process(value: any): any {
  return value;
}

const result = process('hello');  // any (not string!)

// Correct - from specific to general
function process(value: string): string;
function process(value: number): number;
function process(value: any): any;
function process(value: any): any {
  return value;
}

const result = process('hello');  // string

Implementation Signature

Implementation must be compatible with ALL overload signatures.

// Overload signatures
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;

// Implementation must cover both cases
function combine(a: string | number, b: string | number): string | number {
  if (typeof a === 'string' && typeof b === 'string') {
    return a + b;
  }
  if (typeof a === 'number' && typeof b === 'number') {
    return a + b;
  }
  throw new Error('Invalid arguments');
}

const str = combine('Hello, ', 'World');  // string
const num = combine(10, 20);              // number

Important:

Implementation signature is NOT visible externally. Users only see overload signatures.


Overloads vs Union Types

When to Use Overloads

// With overloads - type depends on argument
function parse(data: string): object;
function parse(data: object): string;
function parse(data: string | object): string | object {
  if (typeof data === 'string') {
    return JSON.parse(data);
  }
  return JSON.stringify(data);
}

const obj = parse('{"name":"John"}');  // object
const str = parse({ name: 'John' });   // string

When Union Types Suffice

// Union type is simpler and clearer
function format(value: string | number): string {
  return value.toString();
}

Practical Examples

Array Extraction

function getFirst<T>(arr: T[]): T | undefined;
function getFirst<T>(arr: T[], count: number): T[];
function getFirst<T>(arr: T[], count?: number): T | T[] | undefined {
  if (count === undefined) {
    return arr[0];
  }
  return arr.slice(0, count);
}

const numbers = [1, 2, 3, 4, 5];
const first = getFirst(numbers);       // number | undefined
const firstThree = getFirst(numbers, 3); // number[]

Finding Elements

function find(predicate: (item: string) => boolean): string | undefined;
function find(predicate: (item: string) => boolean, all: true): string[];
function find(
  predicate: (item: string) => boolean,
  all?: boolean
): string | string[] | undefined {
  const items = ['apple', 'banana', 'cherry'];
  
  if (all) {
    return items.filter(predicate);
  }
  return items.find(predicate);
}

const one = find(item => item.startsWith('a'));      // string | undefined
const many = find(item => item.startsWith('a'), true); // string[]

Event Listener

function addEventListener(
  element: HTMLElement,
  event: 'click',
  handler: (e: MouseEvent) => void
): void;

function addEventListener(
  element: HTMLElement,
  event: 'keypress',
  handler: (e: KeyboardEvent) => void
): void;

function addEventListener(
  element: HTMLElement,
  event: string,
  handler: (e: Event) => void
): void {
  element.addEventListener(event, handler);
}

const button = document.querySelector('button')!;

addEventListener(button, 'click', (e) => {
  // e: MouseEvent
  console.log(e.clientX);
});

addEventListener(button, 'keypress', (e) => {
  // e: KeyboardEvent
  console.log(e.key);
});

API Wrapper

function request(method: 'GET', url: string): Promise<Response>;
function request(
  method: 'POST' | 'PUT',
  url: string,
  body: object
): Promise<Response>;

function request(
  method: string,
  url: string,
  body?: object
): Promise<Response> {
  const options: RequestInit = { method };
  
  if (body) {
    options.body = JSON.stringify(body);
    options.headers = { 'Content-Type': 'application/json' };
  }
  
  return fetch(url, options);
}

// Usage
request('GET', '/api/users');
request('POST', '/api/users', { name: 'John' });
// request('POST', '/api/users');  // Error: body required

Class Method Overloads

class DataStore {
  private data: Record<string, any> = {};
  
  // Method overloads
  get(key: 'name'): string;
  get(key: 'age'): number;
  get(key: 'active'): boolean;
  get(key: string): any {
    return this.data[key];
  }
  
  set(key: 'name', value: string): void;
  set(key: 'age', value: number): void;
  set(key: 'active', value: boolean): void;
  set(key: string, value: any): void {
    this.data[key] = value;
  }
}

const store = new DataStore();
store.set('name', 'John');    // OK
store.set('age', 30);         // OK
// store.set('age', 'thirty'); // Error

const name = store.get('name');  // string
const age = store.get('age');    // number

Constructor Overloads

class Point {
  x: number;
  y: number;
  
  constructor(x: number, y: number);
  constructor(coords: { x: number; y: number });
  constructor(xOrCoords: number | { x: number; y: number }, y?: number) {
    if (typeof xOrCoords === 'number') {
      this.x = xOrCoords;
      this.y = y!;
    } else {
      this.x = xOrCoords.x;
      this.y = xOrCoords.y;
    }
  }
}

const p1 = new Point(10, 20);
const p2 = new Point({ x: 10, y: 20 });

Generics in Overloads

function map<T, U>(arr: T[], fn: (item: T) => U): U[];
function map<T, U>(arr: T[], fn: (item: T, index: number) => U): U[];
function map<T, U>(
  arr: T[],
  fn: (item: T, index?: number) => U
): U[] {
  return arr.map(fn as any);
}

const numbers = [1, 2, 3];
const doubled = map(numbers, n => n * 2);
const indexed = map(numbers, (n, i) => `${i}: ${n}`);

Common Mistakes

Incompatible Implementation

// Error: implementation not compatible with overloads
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean {  // Error!
  return value;
}

Forgetting Order

// Bad - general case hides specific ones
function handle(value: any): any;
function handle(value: string): string;  // Unreachable!
function handle(value: any): any {
  return value;
}

Too Many Overloads

// Bad - too complex
function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;
function format(value: Date): string;
function format(value: object): string;
// ... 10 more overloads

// Better use union or generic
function format(value: string | number | boolean | Date | object): string {
  return String(value);
}

Best Practices

Use Overloads Only When Necessary

// Overloads not needed
function add(a: number, b: number): number {
  return a + b;
}

// Overloads needed - different result types
function getValue(key: 'name'): string;
function getValue(key: 'age'): number;
function getValue(key: string): any {
  // ...
}

Keep Overloads Close to Implementation

// Good - all together
function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
  return value;
}

Document Complex Overloads

/**
 * Creates a date from timestamp
 * @param timestamp - Unix timestamp in milliseconds
 */
function makeDate(timestamp: number): Date;

/**
 * Creates a date from year, month, and day
 * @param year - Full year (e.g., 2024)
 * @param month - Month (0-11)
 * @param day - Day of month (1-31)
 */
function makeDate(year: number, month: number, day: number): Date;

function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
  // ...
}

Alternatives to Overloads

Conditional Types

type ReturnType<T> = T extends 'string' ? string :
                     T extends 'number' ? number :
                     T extends 'boolean' ? boolean :
                     never;

function getValue<T extends 'string' | 'number' | 'boolean'>(
  type: T
): ReturnType<T> {
  // ...
  return null as any;
}

Discriminated Unions

type Options = 
  | { type: 'GET'; url: string }
  | { type: 'POST'; url: string; body: object };

function request(options: Options): Promise<Response> {
  // ...
}

Conclusion

Function Overloads:

  • Allow declaring multiple signatures for one function
  • Signature choice depends on arguments
  • Order matters (from specific to general)
  • Implementation must be compatible with all overloads
  • Useful when return type depends on arguments
  • Alternatives: union types, conditional types, discriminated unions
  • Don't overuse — use only when necessary

In Interviews:

Important to be able to:

  • Explain function overload syntax
  • Write example with different return types
  • Explain difference between overload and implementation signatures
  • Show importance of signature order
  • Tell when overloads are better than union types
  • Name alternatives (conditional types, discriminated unions)