TypeScriptFunction Types

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:

TS
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();
Note
Parameter names in function type expressions are required by syntax but not by the type system. (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:

TS
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:

TS
// 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

Tip
Use 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:

TS
// 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:

TS
// 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

TS
// 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:

TS
// 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]

TS
// 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 type
Bivariance 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:

TS
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!
Warning
With 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

TS
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 shape
Practical: Typed Reducer

TS
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); // 3
Avoid 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:

TS
// 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();
}
Success
Replace every 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)