Function Types
Functions are the building blocks of every TypeScript program. TypeScript lets you describe the exact shape of a function — what parameters it accepts, what it returns, and how it behaves with generics. Getting this right unlocks full type-safety across callbacks, higher-order functions, event handlers, and reducers.
Function Type Expressions
The most common way to describe a function type is a function type expression:
type Callback = (err: Error | null, result: string) => void;
function loadFile(path: string, callback: Callback): void {
// ...
}
// The arrow separates parameters from return type
// Parameter names are required but are for documentation only
type Transformer = (input: string) => string;
const upper: Transformer = (s) => s.toUpperCase();(string) => void is a syntax error; you must write (value: string) => void. The name value is ignored during type checking — only the type matters.Parameter Names Are Documentation Only
TypeScript checks function types structurally — only the parameter types and return type matter, not the names. These three types are identical:
type A = (x: number) => string; type B = (value: number) => string; type C = (n: number) => string; // All three describe the same callable — freely assignable to each other const fn: A = (value) => String(value); // fine — name "value" != "x" is ok const fn2: B = fn; // fine const fn3: C = fn2; // fine
void vs undefined Return Types
void and undefined look similar but behave differently in function return types:
// void: the return value will not be used — callers should ignore it type VoidFn = () => void; // A function typed as void CAN return a value — TypeScript discards it const voidFn: VoidFn = () => 42; // OK! TypeScript allows this const result = voidFn(); // result is typed as void, not number // undefined: the function must explicitly return undefined type UndefinedFn = () => undefined; const ok: UndefinedFn = () => undefined; // must return undefined // const bad: UndefinedFn = () => 42; // TS2322 error
Return type | Meaning | Can return a value? |
|---|---|---|
void | Return value is ignored by callers | Yes — the value is discarded |
undefined | Must return undefined explicitly | No — any other value is an error |
never | Function never completes normally | No — it throws or loops forever |
void for callbacks and event handlers where the return value is never read. Use undefined only when you truly need to enforce that the function returns nothing at all.The never Return Type
never means the function never produces a value — it either always throws or
enters an infinite loop. TypeScript uses never for exhaustiveness checks:
// A function that always throws — return type is never
function fail(message: string): never {
throw new Error(message);
}
// Exhaustiveness guard — TS errors if you miss a union member
function assertNever(x: never): never {
throw new Error(`Unhandled value: ${JSON.stringify(x)}`);
}
type Shape = 'circle' | 'square' | 'triangle';
function area(shape: Shape): number {
switch (shape) {
case 'circle': return Math.PI;
case 'square': return 1;
case 'triangle': return 0.5;
default: return assertNever(shape); // TS error if a case is missing
}
}Typing Higher-Order Functions
A higher-order function takes a function as an argument or returns one. TypeScript lets you express this precisely with function types:
// Takes a function, returns a memoized version of it
function memoize<T extends (...args: unknown[]) => unknown>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();
return ((...args: unknown[]) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args) as ReturnType<T>;
cache.set(key, result);
return result;
}) as T;
}
function expensive(n: number): number {
console.log('computing...');
return n * n;
}
const cached = memoize(expensive);
cached(4); // logs "computing..."
cached(4); // from cache — no log// Middleware: takes a handler, returns a handler
type Handler = (req: { url: string; method: string }) => { status: number };
type Middleware = (next: Handler) => Handler;
const withLogging: Middleware = (next) => (req) => {
console.log(`${req.method} ${req.url}`);
return next(req);
};
const withAuth: Middleware = (next) => (req) => {
// check credentials here
return next(req);
};Generic Function Types
When the type of a parameter and the return value are related, use a type parameter:
// Comparator that works for any type T
type Comparator<T> = (a: T, b: T) => number;
const numericAsc: Comparator<number> = (a, b) => a - b;
const alphaAsc: Comparator<string> = (a, b) => a.localeCompare(b);
// Flip any ascending comparator into a descending one
function descending<T>(cmp: Comparator<T>): Comparator<T> {
return (a, b) => cmp(b, a); // swap arguments
}
[3, 1, 4, 1, 5].sort(descending(numericAsc));
// → [5, 4, 3, 1, 1]// A typed map that preserves the element relationship
type Mapper<T, U> = (value: T, index: number, array: readonly T[]) => U;
function typedMap<T, U>(arr: T[], fn: Mapper<T, U>): U[] {
return arr.map(fn as (value: T, index: number, array: T[]) => U);
}
const lengths = typedMap(['hello', 'world'], (s) => s.length);
// lengths: number[] ← TypeScript inferred the return typeBivariance vs Contravariance in Parameters
In strict mode, TypeScript checks function parameters contravariantly. This means a handler for a broad type can stand in for a handler of a narrower type, but not the other way around:
class Animal { name = 'animal' }
class Dog extends Animal { breed = 'labrador' }
type AnimalHandler = (a: Animal) => void;
type DogHandler = (d: Dog) => void;
const handleAnimal: AnimalHandler = (a) => console.log(a.name);
// With strictFunctionTypes:
// AnimalHandler can be used as a DogHandler — it handles any Animal, so it
// can certainly handle a Dog
const h: DogHandler = handleAnimal; // OK — safe
// But the reverse is NOT safe and is rejected in strict mode:
const handleDog: DogHandler = (d) => console.log(d.breed);
// const bad: AnimalHandler = handleDog; // TS2322 — callers might pass a Cat!strictFunctionTypes enabled (part of strict), function parameters are checked contravariantly. Method signatures in classes and interfaces remain bivariant for historical compatibility with older patterns.Practical: Typed Event Handlers
type EventMap = {
click: { x: number; y: number };
keydown: { key: string; ctrlKey: boolean };
resize: { width: number; height: number };
};
type EventHandler<T> = (event: T) => void;
class TypedEmitter<TMap extends Record<string, unknown>> {
private handlers = {} as {
[K in keyof TMap]?: Array<EventHandler<TMap[K]>>
};
on<K extends keyof TMap>(event: K, handler: EventHandler<TMap[K]>): void {
(this.handlers[event] ??= []).push(handler);
}
emit<K extends keyof TMap>(event: K, data: TMap[K]): void {
(this.handlers[event] ?? []).forEach((h) => h(data));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on('click', ({ x, y }) => console.log(`${x}, ${y}`));
emitter.emit('click', { x: 100, y: 200 });
// emitter.emit('click', { key: 's' }); // TS error — wrong shapePractical: Typed Reducer
type Action =
| { type: 'INCREMENT'; by: number }
| { type: 'DECREMENT'; by: number }
| { type: 'RESET' };
type Reducer<S, A> = (state: S, action: A) => S;
type CounterState = { count: number };
const counterReducer: Reducer<CounterState, Action> = (state, action) => {
switch (action.type) {
case 'INCREMENT': return { count: state.count + action.by };
case 'DECREMENT': return { count: state.count - action.by };
case 'RESET': return { count: 0 };
}
};
let state: CounterState = { count: 0 };
state = counterReducer(state, { type: 'INCREMENT', by: 5 });
state = counterReducer(state, { type: 'DECREMENT', by: 2 });
console.log(state.count); // 3Avoid the Function Type
The built-in Function type accepts any callable value and returns any. It
gives up all type safety — avoid it in application code:
// Bad — Function accepts anything and returns any
function apply(fn: Function, value: unknown): unknown {
return fn(value); // result is any — no safety
}
// Good — be specific
function apply<T, R>(fn: (value: T) => R, value: T): R {
return fn(value); // result is R — fully typed
}
// If you need to accept any callable, at least constrain the signature
function call(fn: (...args: unknown[]) => void): void {
fn();
}Function type with a proper function type expression or generic. You will catch argument and return type bugs at the call site thatFunction would silently allow.Quick Reference
(a: T) => U — function type expression, the common form
void — return value is ignored; the function can still return a value
undefined — the function must return undefined only
never — function throws or loops infinitely, never produces a value
Function — avoid; use a specific signature instead
Generic function types: <T>(x: T) => T
Higher-order: (fn: (x: T) => U) => (x: T) => U
Strict mode makes function parameters contravariant (catches real bugs)