Type Callback Functions in TypeScript
A callback is a function passed as an argument to another function. Learn how to type callback parameters and the key rule about optional callback parameters.
A callback is a function passed as an argument to another function. To type callback functions in TypeScript, use a function type expression that describes the parameters and return type the callback must have, the same way you type any function.
function fetchData(callback: (data: string) => void) {
const result = "Fetched data";
callback(result);
}
fetchData((data) => {
console.log(data.toUpperCase());
});
// Output: FETCHED DATAThe callback parameter is typed as a function that takes a string and returns nothing. This means any function passed as the callback must accept a single string argument. The void return type tells callers that the return value, if any, is ignored.
How Callers Benefit From Callback Types
When you type a callback parameter, callers get full type checking and autocomplete on their callback arguments. They do not need to annotate the callback themselves because TypeScript infers the parameter types from the expected callback signature:
function processItems(
items: string[],
transform: (item: string, index: number) => string
): string[] {
return items.map(transform);
}Calling this function with a plain arrow function, and no annotations at all, still gives full type checking because the parameter types come from the transform callback type:
const result = processItems(["a", "b", "c"], (item, index) => {
return `${index}: ${item.toUpperCase()}`;
});
console.log(result);
// Output: ["0: A", "1: B", "2: C"]TypeScript infers the item as a string and the index as a number from the callback type. The caller gets autocomplete on string methods and type errors if they try to use the item as a number.
When the caller makes a mistake, the error points to the callback, not deep inside the function that calls it:
processItems(["a", "b"], (item) => item * 2);Multiplying a string by a number is not a valid operation, so the compiler flags it right where the callback itself is written:
The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.The callback type caught that multiplying the item is not valid on a string. The error appears at the call site where the fix is obvious.
Naming Callback Types With Aliases
When the same callback signature appears in multiple places, or when the signature is complex, use a type alias:
type ErrorHandler = (error: Error, context: string) => void;
function connect(host: string, onError: ErrorHandler) {
try {
console.log(`Connecting to ${host}`);
} catch (err) {
onError(err as Error, "connect");
}
}The same alias works for a second function with a similar failure path, so the signature only needs to be written once:
function disconnect(onError: ErrorHandler) {
try {
console.log("Disconnecting");
} catch (err) {
onError(err as Error, "disconnect");
}
}The alias is used in both functions above. If the error handling signature changes later, you update one type instead of every function signature.
The Void Callback Rule
A callback type with a void return type does not prevent the passed callback from returning a value. The return value is simply ignored:
function forEach<T>(items: T[], callback: (item: T) => void) {
for (const item of items) {
callback(item);
}
}Passing a callback that returns the array's new length, instead of nothing, still compiles because a void callback type never restricts what the callback body can return:
const numbers = [1, 2, 3];
const results: number[] = [];
forEach(numbers, (n) => results.push(n));
console.log(results);
// Output: [1, 2, 3]The callback returns a number (the new array length), but the callback type says void. TypeScript accepts this because the return value is ignored by the calling function. This matches the behavior of Array.forEach in the standard library.
The rule exists for a reason: if void callbacks could not return values, you could not write a forEach callback that pushes into another array, which is a common and correct pattern.
The Critical Optional Parameter Rule
There is one rule about callback types that causes more confusion than any other: never mark a callback parameter as optional unless you sometimes call the callback without that argument.
Consider this incorrect pattern:
// Wrong: index is marked optional in the callback type
function myForEach(
arr: string[],
callback: (item: string, index?: number) => void
) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i); // Always passes i
}
}The optional marker tells TypeScript the callback might be called with only one argument. But the implementation always passes the index. TypeScript will then incorrectly flag it as possibly undefined inside the callback:
myForEach(["a", "b"], (item, index) => {
console.log(index.toFixed());
});Even though the implementation always passes a real index, the optional marker in the type makes the compiler treat it as possibly missing:
'index' is possibly 'undefined'.The fix is to declare the index as required in the callback type. Callers who do not need it can simply omit it from their callback, and TypeScript allows this because a function with fewer parameters is always compatible:
// Correct: index is required in the callback type
function myForEach(
arr: string[],
callback: (item: string, index: number) => void
) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}Callers who do not need the index can still omit it, since a function with fewer parameters is always compatible with a function type expecting more:
// Caller omits index: fine, TypeScript allows fewer parameters
myForEach(["a", "b"], (item) => {
console.log(item);
});
// Output: a
// Output: bCallers who do use the index get a plain number, not a possibly-undefined value, because the callback type now declares it as required:
// Caller uses index: fine, and index is number, not number | undefined
myForEach(["a", "b"], (item, index) => {
console.log(`${index}: ${item}`);
});
// Output: 0: a
// Output: 1: bThe rule is: declare the callback type with all the parameters you actually pass. Let callers omit the ones they do not need.
Callbacks That Accept Multiple Signatures
Sometimes a callback needs to handle more than one shape of data. Use a union type for the callback parameter and narrow inside the callback body:
type Result = { kind: "ok"; value: string } | { kind: "err"; message: string };
function handleResult(
result: Result,
onResult: (result: Result) => void
) {
onResult(result);
}The callback receives the whole union, so it narrows the result inside its own body before reading properties that only exist on one branch:
handleResult({ kind: "ok", value: "loaded" }, (result) => {
if (result.kind === "ok") {
console.log(result.value.toUpperCase());
} else {
console.error(result.message);
}
});
// Output: LOADEDNarrowing on the kind property gives access to the correct properties for each branch. This pattern replaces callback overloads, which are rarely the right solution for typed callbacks.
Common Mistakes
Marking callback parameters as optional. As explained above, only use ? when you actually call the callback without that argument. In practice, this is rare.
Using Function as a callback type. The global Function type accepts any function but gives you no type checking. Prefer a specific function type expression instead:
// Avoid: no type checking on the callback
function register(handler: Function) {
handler("event");
}
// Prefer: full type checking
function register(handler: (event: string) => void) {
handler("event");
}Writing the callback type inline when it is long. If the callback type has more than two parameters, extract it into a type alias. This keeps function signatures scannable.
For related concepts, see type arrow functions in TypeScript and TypeScript function types explained.
Rune AI
Key Insights
- Type a callback parameter with a function type: callback: (x: Type) => ReturnType.
- Contextual typing infers callback parameter types, so callers rarely need annotations.
- Never mark callback parameters as optional unless you actually skip them.
- A void return type on a callback still accepts callbacks that return a value.
- Use a type alias for reusable callback signatures to keep code readable.
Frequently Asked Questions
How do I type a callback parameter in TypeScript?
Should I mark callback parameters as optional?
Can a callback return a value if the type says void?
Conclusion
Typing callback functions is about describing the signature the callback must satisfy. Write the callback type as a function type expression: (arg: Type) => ReturnType. Contextual typing means callers rarely need to annotate their callback parameters. Never mark callback parameters as optional unless you genuinely skip them at the call site. And remember that a void callback type does not prevent the callback from returning a value, it just tells callers the return value is ignored.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.