How to get into BigTech? Top devs share their journey —watch on YouTube
Practice TS Problems

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)
Practice TS Problems