A generic function works with many types while keeping track of which one it was given. In JSDoc, @template T declares the type parameter, and T then appears in @param and @returns like any type. groupBy(products, (p) => p.category) returns Record<string, Product[]> because T is inferred as the product type from the first argument.
The callback is checked against that inferred type, so (p) => p.category on products without a category is TS2339, as the second example shows.
At a glance
| Use | JSDoc in a .js file | TypeScript in a .ts file |
|---|---|---|
| A generic function | /**
* @template T
* @param {T[]} items
* @returns {T | undefined}
*/ | function first<T>(items: T[]): T | undefined |
| Several type parameters | /** @template K, V, R */ | <K, V, R> |
| Using one | /** @type {Map<K, R>} */ | : Map<K, R> |
Examples
/**
* @template T
* @param {T[]} items
* @param {(item: T) => string} keyOf
* @returns {Record<string, T[]>}
*/
export function groupBy(items, keyOf) {
/** @type {Record<string, T[]>} */
const groups = {};
for (const item of items) (groups[keyOf(item)] ??= []).push(item);
return groups;
}
const products = [
{ sku: "TEE-01", category: "shirts", price: 19.5 },
{ sku: "MUG-07", category: "kitchen", price: 12 },
];
const byCategory = groupBy(products, (p) => p.category);
byCategory.shirts?.map((p) => p.price.toFixed(2));export function groupBy<T>(items: T[], keyOf: (item: T) => string): Record<string, T[]> {
const groups: Record<string, T[]> = {};
for (const item of items) (groups[keyOf(item)] ??= []).push(item);
return groups;
}
const products = [
{ sku: "TEE-01", category: "shirts", price: 19.5 },
{ sku: "MUG-07", category: "kitchen", price: 12 },
];
const byCategory = groupBy(products, (p) => p.category);
byCategory.shirts?.map((p) => p.price.toFixed(2));jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /**
* @template T
* @param {T[]} items
* @param {(item: T) => string} keyOf
*/
export function groupBy(items, keyOf) {
return items.map(keyOf);
}
const products = [{ sku: "TEE-01", price: 19.5 }];
groupBy(products, (p) => p.category);error TS2339: Property 'category' does not exist on type '{ sku: string; price: number; }'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /**
* @template K, V, R
* @param {Map<K, V>} map
* @param {(value: V) => R} fn
* @returns {Map<K, R>}
*/
export function mapValues(map, fn) {
return new Map([...map].map(([key, value]) => [key, fn(value)]));
}
const prices = new Map([["TEE-01", 19.5]]);
const labels = mapValues(prices, (price) => `$${price.toFixed(2)}`);
labels.get("TEE-01")?.toUpperCase();jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Inside the function
In the body, T is unknown in the sense that it could be anything, so you can only do with a T what works for every type: store it, return it, pass it on. To read properties from it, give T a constraint with @template {{ id: string }} T.
A local variable can use the type parameter too: /** @type {Record<string, T[]>} */ const groups = {}; in the first example.
Several type parameters
List them with commas, @template K, V, R, or give each its own @template line. Each one is inferred separately; in the third example K and V come from the Map and R from what the callback returns.
Gotchas
- Callers cannot write type arguments in a
.jsfile; inference does all the work. - A
@templatetag must be in the same comment as the function's other tags.
Questions
How do I write a generic function in JSDoc?
Add @template T to the function's comment and use T in its @param and @returns types. TypeScript infers T from the arguments at each call.
Sources
- Generics · TypeScript handbook
- JSDoc Reference: @template · TypeScript handbook
Last reviewed 20 October 2026.