Function Types, Overloads & Optional/Default Parameters

10 questions found

Use arrow-style syntax `(param: Type) => ReturnType` to describe a function's shape, specifying each parameter's type and the return type.
function process(callback: (value: number) => void) {
  callback(42);
}
process((n) => console.log(n));
Real-world example Typing a callback parameter passed to an array method wrapper or an event handler registration function.

Common follow-ups: How do you write a function type as a named 'type' alias instead of inline?

Generics
Add a '?' after the parameter name; TypeScript automatically adds 'undefined' to its type, and optional parameters must come after all required parameters in the parameter list.
function greet(name: string, title?: string) {
  return title ? `${title} ${name}` : name;
}
greet('Sam');           // 'Sam'
greet('Sam', 'Dr.');    // 'Dr. Sam'
Real-world example Making a configuration parameter optional so callers can omit it and rely on default in-function behavior.

Common follow-ups: Can an optional parameter come before a required parameter in the list?

Nullability: strictNullChecks Optional Chaining & Nullish Coalescing
Assign a default value directly in the parameter list using '='; TypeScript infers the parameter's type from the default value's type unless you also add an explicit annotation, and the parameter becomes optional to callers.
function greet(name: string, greeting = 'Hello') {
  return `${greeting}, ${name}`;
}
greet('Sam');           // 'Hello, Sam'
greet('Sam', 'Hi');     // 'Hi, Sam'
Real-world example Providing a sensible default page size or sort order for a paginated query function.

Common follow-ups: Can a default parameter's value reference an earlier parameter in the same function?

Type Inference & Contextual Typing
You declare multiple overload signatures (no implementation) followed by one general implementation signature that's compatible with all of them; TypeScript uses the overload signatures for type-checking calls, picking the first matching one, while the implementation signature is only used internally and isn't visible to callers.
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
  return month !== undefined
    ? new Date(yearOrTimestamp, month, day!)
    : new Date(yearOrTimestamp);
}
makeDate(2026);           // uses first overload
makeDate(2026, 7, 8);     // uses second overload
Real-world example Supporting both a single-timestamp and a year/month/day style call for a date-construction utility, each with precise argument-count checking.

Common follow-ups: What error occurs if you call the function with arguments that don't match ANY of the overload signatures?

Union & Intersection Types
A rest parameter (prefixed with '...') collects any remaining arguments into an array, so its type annotation must be an array type (or tuple type for fixed-shape variadic arguments), matching how rest parameters behave in plain JavaScript.
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6
Real-world example Typing a logging function that accepts a variable number of arguments of the same type.

Common follow-ups: Can you type a rest parameter as a tuple to enforce a fixed minimum number of arguments?

Primitive Types Arrays & Tuples
A function type A is assignable to function type B if A's parameters are compatible with B's (contravariantly, roughly) AND A doesn't require MORE parameters than B provides — meaning a function taking fewer parameters can safely stand in for one expecting more, since extra arguments are simply ignored, but not the reverse.
type Handler = (event: Event, index: number) => void;
const handler: Handler = (event) => console.log(event); // OK: fewer params allowed
// const bad: Handler = (event, index, extra) => {}; // Error: too many required params
Real-world example Passing a simple callback like `(item) => ...` where an array method expects `(item, index, array) => ...`.

Common follow-ups: Why does this asymmetry exist — why can a function with FEWER parameters satisfy a type expecting MORE?

Structural Typing & Duck Typing
Combine overload signatures with specific literal parameter types for each variant, letting the return type or accepted shape change based on that literal — commonly used for APIs like document.createElement, where the tag name literal determines the specific returned element type.
function createElement(tag: 'a'): HTMLAnchorElement;
function createElement(tag: 'img'): HTMLImageElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}
const link = createElement('a'); // typed as HTMLAnchorElement
Real-world example Precisely typing a factory function whose return shape depends on a specific string literal argument, like DOM element creation.

Common follow-ups: Could this same effect be achieved with a single generic function and conditional types instead of overloads?

Conditional Types
TypeScript resolves overloads by picking the FIRST signature (top to bottom) that matches the call — if a broader, less specific overload is listed before a more specific one, it will always match first, silently shadowing the more specific overload and giving callers a less precise type than intended.
// WRONG ORDER: general overload shadows the specific one
function process(value: unknown): string;
function process(value: number): number; // unreachable! unknown already matched
function process(value: any): any { /* ... */ }

process(42); // incorrectly resolves to the 'unknown' overload, returns string type
Real-world example Debugging why a specific overload's more precise return type never gets applied, tracing it to overload ordering.

Common follow-ups: Does the TypeScript compiler warn you when an overload signature is unreachable like this?

Never Unknown & Void Types
An explicit `this: SomeType` as the FIRST parameter (removed from the actual call signature) tells TypeScript what type 'this' must be when the function is called, letting the compiler catch incorrect method extraction or binding — like passing an object method as a bare callback where 'this' would be lost.
interface Button {
  label: string;
  onClick(this: Button, event: Event): void;
}
const button: Button = {
  label: 'Save',
  onClick(this: Button, event) {
    console.log(this.label); // 'this' is correctly typed as Button here
  }
};
Real-world example Catching a bug where a class method is passed as a raw callback (losing its 'this' binding) before it causes a runtime error.

Common follow-ups: How does this relate to needing .bind(this) or an arrow function for event handlers in classes?

this & Binding
Use generics with the built-in Parameters<T> and ReturnType<T> utility types (or simple generic inference) to derive the wrapped function's exact parameter and return types automatically, producing full type safety without manually re-declaring the shape.
function withLogging<Args extends unknown[], R>(
  fn: (...args: Args) => R
): (...args: Args) => R {
  return (...args: Args): R => {
    console.log('calling with', args);
    return fn(...args);
  };
}
const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add); // inferred as (a: number, b: number) => number
Real-world example Writing a generic, fully type-safe decorator-like wrapper (logging, timing, memoization) that preserves the original function's exact signature.

Common follow-ups: How would Parameters<typeof fn> and ReturnType<typeof fn> be used as an alternative to the generic-inference approach shown here?

Generics