Chapter 5

Function Types: Overloads, Generics, Call Signatures

Two Ways to Write a Function Type

TypeScript offers two syntaxes for function types โ€” both describe the same thing, but they serve different purposes:

// Style 1: arrow syntax (most common, used in type aliases)
type Add = (a: number, b: number) => number;

// Style 2: call signature / object syntax (used inside interface/type objects)
type Add2 = {
  (a: number, b: number): number;
};

// Both are used identically
const add: Add  = (a, b) => a + b;
const add2: Add2 = (a, b) => a + b;

The object syntax can carry properties alongside the call signature โ€” which arrow syntax cannot. This matters for callable objects (covered later in this chapter).

Parameter Modifiers: Optional, Default, Rest

// Optional parameter: ? makes the type T | undefined
function greet(name: string, greeting?: string): string {
  return `${greeting ?? "Hello"}, ${name}!`;
}

greet("Alice");           // "Hello, Alice!"
greet("Alice", "Hi");    // "Hi, Alice!"

// Default value: the caller never sees undefined โ€” it's handled inside
function createUser(name: string, role: string = "user") {
  return { name, role };
}

createUser("Alice");            // { name: "Alice", role: "user" }
createUser("Bob", "admin");     // { name: "Bob", role: "admin" }

// Rest parameters: typed as an array
function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}

sum(1, 2, 3, 4);  // 10

// Rest parameters can also be a tuple type (TypeScript 4.0+)
function log(message: string, ...tags: [string, ...string[]]): void {
  console.log(`[${tags.join("][")}] ${message}`);
}

log("Server started", "info", "startup");

Optional vs. default โ€” the difference matters:

// Optional parameter: passing undefined is explicit and valid
function withOptional(x?: number) {
  // x is number | undefined inside the function
  return x ?? 0;
}

// Default parameter: passing undefined triggers the default
function withDefault(x: number = 0) {
  // x is number โ€” undefined is already handled
  return x;
}

withOptional(undefined);  // OK, returns 0
withDefault(undefined);   // OK, returns 0 (default applied)

Function Overloads: One Name, Multiple Call Shapes

Overloads let one function return different types depending on how it is called. Overload signatures declare all legal call forms. The implementation signature is the actual code โ€” it must handle every overload, but it is not directly callable from outside.

// Overload signatures (visible to callers)
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: "input"): HTMLInputElement;
// Implementation signature (not callable directly โ€” must cover all overloads)
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const div   = createElement("div");    // Type: HTMLDivElement
const span  = createElement("span");   // Type: HTMLSpanElement
const input = createElement("input");  // Type: HTMLInputElement
// createElement("section");           // Error: no matching overload

The implementation signature is invisible to callers โ€” a very common source of confusion:

function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  if (typeof value === "string") {
    return value.trim();
  }
  return value.toFixed(2);
}

format("hello");   // OK
format(3.14);      // OK
// format(true);   // Error: boolean does not match any overload

Overloading on parameter count:

function getDate(): Date;
function getDate(timestamp: number): Date;
function getDate(timestamp?: number): Date {
  return timestamp !== undefined ? new Date(timestamp) : new Date();
}

const now   = getDate();
const fixed = getDate(1700000000000);

Anti-Pattern: Using Overloads When a Union Would Work

Many overloads are unnecessary. Reach for overloads only when different argument combinations produce different return types that a union cannot express.

// Anti-pattern: overloads when the return type does not change
function print(value: string): void;
function print(value: number): void;
function print(value: string | number): void {
  console.log(String(value));
}

// Correct: a plain union parameter is clearer
function print(value: string | number): void {
  console.log(String(value));
}

A legitimate overload case โ€” the return type changes based on a boolean flag, and a plain union cannot express the pairing:

function parseCSV(data: string, asArray: true): string[];
function parseCSV(data: string, asArray?: false): string;
function parseCSV(data: string, asArray?: boolean): string | string[] {
  const result = data.split(",").map(s => s.trim());
  return asArray ? result : result.join(", ");
}

const arr: string[] = parseCSV("a, b, c", true);
const str: string   = parseCSV("a, b, c");

Generic Functions: <T> for Type-Agnostic Logic

When a function's logic does not depend on a specific type, use a generic parameter instead of any. Generics preserve type information; any discards it.

// Bad: any loses type information
function first(arr: any[]): any {
  return arr[0];
}

const x = first([1, 2, 3]);  // x is any โ€” no more type protection

// Good: generic preserves type information
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const n = first([1, 2, 3]);      // number | undefined
const s = first(["a", "b"]);     // string | undefined
const u = first([]);             // undefined

Multiple type parameters:

function zip<A, B>(as: A[], bs: B[]): [A, B][] {
  return as.map((a, i) => [a, bs[i]] as [A, B]);
}

const pairs = zip([1, 2, 3], ["a", "b", "c"]);
// pairs: [number, string][]  โ†’  [[1,"a"],[2,"b"],[3,"c"]]

Constrained generics:

// T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "hi");        // "hello" โ€” type: string
longest([1, 2, 3], [1, 2]);    // [1, 2, 3] โ€” type: number[]
// longest(1, 2);               // Error: number has no length property

// K must be a key of T โ€” ensures safe property access
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name");    // string
const id   = getProperty(user, "id");      // number
// getProperty(user, "email");             // Error: not a key of user

The this Parameter

In regular (non-arrow) functions, you can explicitly type this as the first parameter. It is a phantom parameter โ€” it disappears at runtime and does not affect the call signature:

interface Button {
  label: string;
  onClick(this: Button): void;
}

const btn: Button = {
  label: "Click me",
  onClick(this: Button) {
    console.log(this.label);  // TypeScript knows this is Button
  },
};

btn.onClick();  // OK

const handler = btn.onClick;
handler();  // Error: void this context is not assignable to Button this

// Arrow functions capture the outer this โ€” no declaration needed
class Counter {
  count = 0;

  increment = (): void => {
    this.count++;  // Arrow function โ€” this is always the Counter instance
  };
}

void vs undefined: A Subtle but Real Difference

// void: callers should not use the return value,
//       but the function body may return any value
type VoidFn = () => void;

// This is legal! void means "ignore the return value"
const arr = [1, 2, 3];
const result: number[] = [];

// forEach expects () => void โ€” push returns number, but void accepts that
arr.forEach(x => result.push(x));  // No error

// undefined: the function must explicitly return undefined (or nothing at all)
type UndefinedFn = () => undefined;

function noReturn(): undefined {
  // return;            // Error: must return undefined, not void
  // return "string";   // Error
  return undefined;    // Required
}

The practical rule:

// Function declarations: void allows bare return
function log(msg: string): void {
  console.log(msg);
  return;            // OK
  // return 1;       // Error
}

// Type alias void is more permissive โ€” intentional, for callback compatibility
const fn: () => void = () => 42;  // OK โ€” callers must ignore the value anyway

// undefined in a type alias is strict
const fn2: () => undefined = () => 42;  // Error: number is not assignable to undefined

Use void for callbacks and event handlers where the return value is irrelevant. Use undefined only when you need to enforce that the function returns nothing meaningful.

Call Signatures: Callable Objects with Properties

A call signature lets you describe an object that is both callable and has properties โ€” something a plain function type cannot express:

interface Logger {
  (message: string): void;         // call signature
  level: "info" | "error" | "warn";
  prefix: string;
}

function createLogger(level: Logger["level"], prefix: string): Logger {
  const fn = ((message: string) => {
    console.log(`[${fn.prefix}][${fn.level}] ${message}`);
  }) as Logger;

  fn.level = level;
  fn.prefix = prefix;

  return fn;
}

const logger = createLogger("info", "APP");
logger("Server started");          // Call it like a function
console.log(logger.level);         // Access it like an object: "info"

Multiple call signatures in one interface (effectively overloads on the object):

interface Formatter {
  (value: string): string;
  (value: number): string;
  locale: string;
}

const fmt = ((value: string | number) => {
  return typeof value === "number" ? value.toFixed(2) : value.trim();
}) as Formatter;

fmt.locale = "en-US";

fmt("  hello  ");   // "hello"
fmt(3.14159);       // "3.14"

Key Takeaways

Feature Key Point Example
Function type syntax Arrow vs object (call signature) (a: T) => R vs { (a: T): R }
Optional param Makes type T | undefined (x?: number)
Default param Caller sees T, not T | undefined (x = 0)
Rest params Typed as array or tuple (...args: number[])
Overloads Implementation sig is hidden; use union when return type is the same Multiple sigs + one impl
Generic functions Preserve types; constrain with extends <T extends U>(x: T): T
this parameter Phantom param โ€” enforces call context (this: MyClass)
void vs undefined void ignores return value; undefined enforces it Callbacks โ†’ void
Call signature Callable objects with properties interface Fn { (x: T): R; prop: P }

Next chapter: advanced generics โ€” generic classes, conditional types, and the infer keyword.

Rate this chapter
4.9  / 5  (64 ratings)

๐Ÿ’ฌ Comments