Functions & Overloads

Difficulty: Beginner

Question

How do you type functions in TypeScript? Explain function overloads and when you'd use them.

Answer

TypeScript provides multiple ways to type functions: inline annotations, type aliases, interfaces, and overload signatures.

Function overloads let you define multiple call signatures for a single function. The compiler picks the matching signature based on the arguments. This is useful when a function behaves differently based on input types.

Key concepts: - Overload signatures (declarations) describe the public API - Implementation signature must be compatible with all overloads - The implementation signature is NOT directly callable - Generic functions are often a cleaner alternative to overloads

Code examples

Function Type Annotations

// Inline function types
const greet: (name: string) => string = (name) => `Hello, ${name}!`;

// Type alias for functions
type MathOp = (a: number, b: number) => number;
const add: MathOp = (a, b) => a + b;
const subtract: MathOp = (a, b) => a - b;

// Interface for callable objects
interface Formatter {
  (input: string): string;
  locale: string;
}

// Callback typing
function fetchData(url: string, onSuccess: (data: unknown) => void): void {
  fetch(url)
    .then(r => r.json())
    .then(onSuccess);
}

// Functions returning promises
async function getUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

Type aliases make function types reusable. Always type callback parameters to ensure callers pass the right function shape.

Function Overloads

// Overload signatures
function createElement(tag: 'a'): HTMLAnchorElement;
function createElement(tag: 'canvas'): HTMLCanvasElement;
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: string): HTMLElement;
// Implementation signature (not directly callable)
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const link = createElement('a');       // HTMLAnchorElement
const canvas = createElement('canvas'); // HTMLCanvasElement
const div = createElement('div');       // HTMLElement

// Another example: different return types based on input
function parse(input: string): string[];
function parse(input: string[]): string;
function parse(input: string | string[]): string | string[] {
  if (typeof input === 'string') {
    return input.split(',');
  }
  return input.join(',');
}

const arr = parse('a,b,c');    // string[]
const str = parse(['a', 'b']); // string

Overloads provide precise return types based on input. The implementation must handle all overload cases but its signature is hidden from callers.

Generic Functions vs Overloads

// Overloads approach (verbose)
function wrap(value: string): { value: string };
function wrap(value: number): { value: number };
function wrap(value: boolean): { value: boolean };
function wrap(value: string | number | boolean) {
  return { value };
}

// Generic approach (cleaner)
function wrapGeneric<T>(value: T): { value: T } {
  return { value };
}
const a = wrapGeneric('hello');  // { value: string }
const b = wrapGeneric(42);       // { value: number }
const c = wrapGeneric(true);     // { value: boolean }

// this parameter
interface Button {
  label: string;
  onClick(this: Button): void;
}
const btn: Button = {
  label: 'Submit',
  onClick() {
    console.log(this.label); // 'this' is typed as Button
  },
};

Prefer generics when the relationship between input and output is consistent. Use overloads only when the return type genuinely differs based on specific inputs.

Key points

Concepts covered

Function Types, Overloads, Generic Functions, Callbacks, this Parameter