Typing functions, callbacks and this

Parameters, optional and rest arguments, overloads that model real call shapes, contextual typing for callbacks, and the this parameter.

Signatures, optional and rest parameters

// a reusable function type: write the shape once, use it everywhere
type Mapper = (value: string, index: number) => number;

function repeat(value: string, times = 1, separator?: string): string {
  return Array.from({ length: times }, () => value).join(separator ?? '');
}

// rest parameters collect the remainder into a real array
function sum(...values: number[]): number {
  return values.reduce((total, value) => total + value, 0);
}

// an optional callback still has to be checked before you call it
function run(task: () => void, onDone?: () => void): void {
  task();
  onDone?.();
}

// annotate the return type of exported functions so a change breaks the build here
export function parsePort(input: string): number {
  const port = Number(input);
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
    throw new RangeError('invalid port: ' + input);
  }
  return port;
}

const double: Mapper = (value) => Number(value) * 2;   // parameter types come from the alias
  • Annotate parameters, which callers depend on; let the return type be inferred unless the function is exported or recursive.
  • Optional parameters must come after required ones, and their type already includes undefined.
  • A recursive function needs an explicit return annotation, otherwise the recursive call is implicitly any.
  • Prefer unknown over any for a parameter whose shape you do not know yet - it forces a check at the call site.

Overloads and contextual typing

// overload signatures describe the call shapes the caller may use...
function parse(input: string): number;
function parse(input: string[]): number[];
// ...and the implementation signature must accept all of them
function parse(input: string | string[]): number | number[] {
  if (Array.isArray(input)) return input.map(Number);
  return Number(input);
}

const one = parse('42');          // number
const many = parse(['1', '2']);   // number[]

// a generic signature is often shorter than a set of overloads
function first<T>(items: T[]): T | undefined {
  return items[0];
}

// a callback passed directly is checked contextually, so annotate nothing
[1, 2, 3].map((value) => value.toFixed(1));

// assigning it to a variable first loses that context
const format = (value: number): string => value.toFixed(1);
[1, 2, 3].map(format);

// a DOM handler receives its event type from the attribute it is bound to
function onSubmit(event: React.FormEvent<HTMLFormElement>): void {
  event.preventDefault();
  const data = new FormData(event.currentTarget);
  console.log(data.get('email'));
}
WhatAnnotate it?Why
ParametersAlwaysThey are the contract callers rely on
Exported return typeYesA change then fails where it was introduced
Local arrow bodyNoInference from the body is accurate
Callback parameterOnly when stored in a variable firstContextual typing already supplies it inline
thisOnly for methods used detached from their objectErased at runtime, checked at compile time
⚠️
Avoid Function and (...args: any[]) => any in a public signature. They accept anything and return nothing usable, so every call site loses its checking - that annotation is worse than leaving the parameter untyped.

The this parameter and detached methods

class Counter {
  count = 0;

  // a fake first parameter: erased at runtime, checked at compile time
  increment(this: Counter, by: number): void {
    this.count += by;
  }
}

const counter = new Counter();
const detach = counter.increment;
// detach(1);                     // error: this would be undefined
detach.call(counter, 1);          // ok

// the everyday fix: an arrow property captures the instance once
class Toggle {
  on = false;
  flip = (): void => { this.on = !this.on; };
}

const toggle = new Toggle();
const flip = toggle.flip;
flip();                           // works, because this is bound to the instance
  • The this parameter exists only for the compiler; it disappears from the emitted JavaScript.
  • Arrow properties bind this once, at construction, which is why event handlers are usually written that way.
  • Keep noImplicitThis on - it is part of strict - so a method that reads this without a context is an error rather than any.

FAQ

When is an overload better than a union?
When the parameter type determines the return type and each call site should get the exact result. Two shapes read well as overloads; five chained calls are clearer as a single generic signature.
Why does my callback need an annotation the array type should provide?
Contextual typing only applies while the function is passed directly. Storing it in a variable first breaks that link, so write the parameter types on the variable.

Narrowing, unions and exhaustive checks Generic patterns without over-engineering

Last refreshed 2026-09-18.