The short answer
Name the callback’s type with @callback, listing its parameters and return type like a function’s, and use that name in the @param tag. For a small callback, an inline arrow type does the same job:
/**
* @typedef {object} Order
* @property {string} id
* @property {number} total
*/
/**
* @callback OrderTest
* @param {Order} order
* @param {number} index
* @returns {boolean}
*/
/**
* @param {Order[]} orders
* @param {OrderTest} keep
* @returns {Order[]}
*/
export function pickOrders(orders, keep) {
return orders.filter(keep);
}
/**
* @param {Order[]} orders
* @param {(order: Order) => void} onEach
*/
export function forEachOrder(orders, onEach) {
for (const order of orders) onEach(order);
}
const orders = [
{ id: "A-1001", total: 40 },
{ id: "A-1002", total: 12 },
];
pickOrders(orders, (order) => order.total > 25);
forEachOrder(orders, (order) => console.log(order.id));export interface Order {
id: string;
total: number;
}
type OrderTest = (order: Order, index: number) => boolean;
export function pickOrders(orders: Order[], keep: OrderTest): Order[] {
return orders.filter(keep);
}
export function forEachOrder(orders: Order[], onEach: (order: Order) => void): void {
for (const order of orders) onEach(order);
}
const orders = [
{ id: "A-1001", total: 40 },
{ id: "A-1002", total: 12 },
];
pickOrders(orders, (order) => order.total > 25);
forEachOrder(orders, (order) => console.log(order.id));jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} The arrow functions passed to pickOrders and forEachOrder need no JSDoc of their own: order is an Order because the parameter’s type says so.
Why it works
@callback OrderTest declares a function type, the same as @typedef {(order: Order, index: number) => boolean} OrderTest, but with room for a description on every line. When a function literal is passed where that type is expected, TypeScript types its parameters from the context. That is why order.total is checked without an annotation, and why the callback’s return value is checked against boolean.
The same thing in TypeScript
The TypeScript equivalent tab declares type OrderTest = (order: Order, index: number) => boolean; and the inline (order: Order) => void as a parameter annotation. A @callback block and a function type alias are interchangeable; tsc treats them the same.
What goes wrong
Because the callback’s parameter is typed from the context, a misspelled property inside it is caught where you write it (TS2551):
/** @typedef {{ id: string, total: number }} Order */
/**
* @callback OrderTest
* @param {Order} order
* @returns {boolean}
*/
/**
* @param {Order[]} orders
* @param {OrderTest} keep
*/
export function pickOrders(orders, keep) {
return orders.filter(keep);
}
pickOrders([], (order) => order.totl > 25);error TS2551: Property 'totl' does not exist on type 'Order'. Did you mean 'total'?jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} And the return type is part of the contract. A callback that returns the order’s id where a boolean is expected fails with TS2322:
/** @typedef {{ id: string, total: number }} Order */
/**
* @callback OrderTest
* @param {Order} order
* @returns {boolean}
*/
/**
* @param {Order[]} orders
* @param {OrderTest} keep
*/
export function pickOrders(orders, keep) {
return orders.filter(keep);
}
pickOrders([], (order) => order.id);error TS2322: Type 'string' is not assignable to type 'boolean'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
- A
@callbackwithout@returnsreturnsany. The type stays open, so nothing checks what the callback gives back. Add@returns {void}when the result is ignored. - Fewer parameters are fine. A callback that ignores
indexcan leave it out, as(order) => …does above; JavaScript passes it anyway, and TypeScript allows it. @callbacknames are types, not values. Use them inside braces in JSDoc. In a.jsfile a@callbackwritten at the top level of a module can be imported into another file with@import, like a@typedef.
Questions
When should I use @callback instead of an inline function type?
Use @callback when the callback has several parameters, needs descriptions for each, or is used in more than one place. For a one-off with a parameter or two, an inline type such as {(order: Order) => void} is shorter and just as checked.
How do I type a Node.js-style callback with an error first?
Give the first parameter a union with null: @param {Error | null} err and @param {string} [data] in a @callback, or inline {(err: Error | null, data?: string) => void}. Check err before using data.
What if the callback's return value does not matter?
Say void: @returns {void} in the @callback, or => void inline. A function that returns something is still accepted where a void callback is expected; the value is just ignored.
Sources
- JSDoc Reference (@callback) · TypeScript handbook
- More on Functions (function type expressions) · TypeScript handbook
- Callback function · MDN Web Docs
Last reviewed 21 October 2026.