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).
| Concept | What it controls |
|---|---|
| Function type | Parameter and return shape |
| Optional params | ? and default values |
| Rest params | ...args tuple typing |
| Overloads | Multiple call signatures, one implementation |
| Covariance | Return types follow subtyping |
| Contravariance | Parameter 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 overloadReal-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 implementationCommon 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 safeThe 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 differencestrictFunctionTypes 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.
strictFunctionTypescatches 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 tuplePractical 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