Topics
v5.0.0
On this page · sections

Functions & Variance

Function types are one of the most common shapes in frontend TypeScript — event handlers, callbacks, hooks, and API clients all revolve around them. Senior interviews go beyond syntax into overloads, optional/rest parameters, and variance (how subtyping works for parameters vs return types).

ConceptWhat it controls
Function typeParameter and return shape
Optional params? and default values
Rest params...args tuple typing
OverloadsMultiple call signatures, one implementation
CovarianceReturn types follow subtyping
ContravarianceParameter types flip subtyping direction

Function type syntax

Definition

A function type names parameters and a return type:

type ClickHandler = (event: MouseEvent) => void; type Fetcher<T> = (url: string, signal?: AbortSignal) => Promise<T>; type Reducer<S, A> = (state: S, action: A) => S;

Arrow syntax and interface call signatures are equivalent for object shapes:

interface Logger { (message: string, level?: "info" | "warn"): void; } const log: Logger = (message, level = "info") => { console[level](message); };

Interview: Function types describe callability — they’re not the same as a method on an object ({ log: () => void } vs () => void).

Optional and default parameters

Definition

Optional parameters use ? or a default value. TypeScript treats both as possibly undefined at the type level:

function greet(name: string, title?: string) { return title ? `${title} ${name}` : name; } function paginate(page = 1, size = 20) { return { page, size }; } type GreetFn = (name: string, title?: string) => string;

Real-world example — React event handlers

type SubmitHandler = ( data: FormData, options?: { reset?: boolean } ) => void | Promise<void>; const onSubmit: SubmitHandler = async (data, options) => { await api.save(data); if (options?.reset) form.reset(); };

Rule: Required parameters cannot follow optional ones unless defaults are used. Put optional params at the end.

Rest parameters

Definition

Rest parameters collect remaining arguments into a tuple:

function sum(...nums: number[]): number { return nums.reduce((a, b) => a + b, 0); } type LogFn = (level: "info" | "error", ...args: unknown[]) => void;

Tuple rest for typed APIs

type EventEmitter = { emit<E extends string>( event: E, ...payload: E extends "click" ? [MouseEvent] : [unknown] ): void; };

Rest parameters pair well with generics when forwarding arguments:

function withLogging<T extends (...args: never[]) => unknown>( fn: T, label: string ): T { return ((...args: Parameters<T>) => { console.log(label, args); return fn(...args); }) as T; }

Function overloads

Definition

Overloads declare multiple call signatures for one implementation. Callers see the overloads; the body uses a single compatible signature.

function format(value: string): string; function format(value: number): string; function format(value: string | number): string { return String(value); } format("hi"); // ✅ string overload format(42); // ✅ number overload format(true); // ❌ no matching overload

Real-world example — createElement-style API

function createRoute(path: "/"): HomeRoute; function createRoute(path: `/users/${string}`): UserRoute; function createRoute(path: string): BaseRoute { return { path, render: () => null }; } const home = createRoute("/"); const user = createRoute("/users/[id]");

Implementation rules

Overload signatures → what callers may pass Implementation → must accept all overload cases (often a union) Return type → inferred from overloads at call site, not implementation

Common pitfall: implementation parameter types are wider than any single overload. Use a union or unknown + narrowing inside the body.

function parse(input: string): number; function parse(input: string[]): number[]; function parse(input: string | string[]): number | number[] { return Array.isArray(input) ? input.map(Number) : Number(input); }

Interview: Prefer generics over overloads when the relationship is “same logic, different type parameter.” Use overloads when return type depends on literal argument shape (e.g. document.getElementById).

Covariance (return types)

Definition

A function type is covariant in its return type: if Dog extends Animal, then () => Dog is assignable to () => Animal.

type Animal = { name: string }; type Dog = Animal & { breed: string }; const getDog = (): Dog => ({ name: "Rex", breed: "Lab" }); const getAnimal: () => Animal = getDog; // ✅ narrower return is safe

The caller only receives the return value — giving them a more specific type is safe.

Array and Promise analogy

const dogs: Dog[] = [{ name: "Rex", breed: "Lab" }]; const animals: Animal[] = dogs; // ✅ arrays are covariant in element type (with caveats for mutation)

Contravariance (parameter types)

Definition

Under strictFunctionTypes, function parameters are contravariant: the parameter position flips the subtyping direction.

type Handler = (event: MouseEvent | KeyboardEvent) => void; const onKeyboard: (event: KeyboardEvent) => void = (e) => { console.log(e.key); }; // ❌ strictFunctionTypes — Handler may be called with MouseEvent const handler: Handler = onKeyboard;

A Handler might receive a MouseEvent. onKeyboard only accepts KeyboardEvent — unsafe if assigned.

Safe direction

const broad: Handler = (event) => { console.log(event.type); }; const narrow: (event: KeyboardEvent) => void = broad; // ❌ still unsafe // Widen the implementation to accept the union: const safeOnKeyboard: Handler = (event) => { if ("key" in event) console.log(event.key); };

Interview: Parameters are inputs — the callee must accept everything the caller might pass. Return types are outputs — the caller can handle a subtype.

Methods vs function properties (bivariance)

Definition

TypeScript treats method syntax on interfaces as bivariant in parameters (historical compatibility). Function property syntax uses strict contravariance.

interface Options { onClick(event: MouseEvent): void; // method — bivariant params onKeyDown?: (event: KeyboardEvent) => void; // property — contravariant }

This is why strictFunctionTypes alone doesn’t catch every unsafe callback assignment on class methods.

Senior guidance

Prefer function property syntax for callbacks in new interfaces Use method syntax when mirroring DOM / library APIs In React props, both patterns appear — know the variance difference

strictFunctionTypes in practice

Definition

Part of strict: true, this flag enforces contravariant parameter checking for function types:

{ "compilerOptions": { "strict": true } }

Real-world example — library callbacks

type Middleware = ( req: Request, next: (req: Request) => Response ) => Response; // A middleware that narrows req internally must still accept full Request const auth: Middleware = (req, next) => { if (!req.headers.get("authorization")) { return new Response("Unauthorized", { status: 401 }); } return next(req); };

Senior Interview Summary

What to say

Function types separate what you accept (contravariant) from what you return (covariant). Overloads model multiple call shapes; optional and rest parameters model flexible arity. strictFunctionTypes catches unsafe callback assignments.

Variance cheat sheet

Return type → covariant (subtype return OK) Param type → contravariant (must accept supertype input) Method syntax → bivariant params (legacy exception)

When to use overloads vs generics

Return depends on literal arg shape → overloads Same logic, type param varies → generics Forwarding args → Parameters<T> / rest tuple

Practical rule

Callback props → accept the widest event type you'll receive Library boundaries → enable strictFunctionTypes Overload APIs → keep implementation signature as union Avoid → assigning narrow-param handlers to wide-param slots

Last updated on