Type expression

Generics with @template

  • Generics
  • Tested with TypeScript 5.9

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

group.js group.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @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));
A generic function: the type follows the argument.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
group.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: group.js(12,28): error TS2339: Property 'category' does not exist on type '{ sku: string; price: number; }'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. group.js(12,28): error TS2339: Property 'category' does not exist on type '{ sku: string; price: number; }'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. group.js(12,28): error TS2339: Property 'category' does not exist on type '{ sku: string; price: number; }'. checked 21 October 2026
/**
 * @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; }'.
The callback is checked against the inferred type.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
map.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @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();
Two type parameters.
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 .js file; inference does all the work.
  • A @template tag 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

Last reviewed 20 October 2026.