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
unknownoveranyfor 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'));
}| What | Annotate it? | Why |
|---|---|---|
| Parameters | Always | They are the contract callers rely on |
| Exported return type | Yes | A change then fails where it was introduced |
| Local arrow body | No | Inference from the body is accurate |
| Callback parameter | Only when stored in a variable first | Contextual typing already supplies it inline |
this | Only for methods used detached from their object | Erased 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
thisparameter exists only for the compiler; it disappears from the emitted JavaScript. - Arrow properties bind
thisonce, at construction, which is why event handlers are usually written that way. - Keep
noImplicitThison - it is part ofstrict- so a method that readsthiswithout a context is an error rather thanany.
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.
Related
Narrowing, unions and exhaustive checks Generic patterns without over-engineering
Last refreshed 2026-09-18.