Recipe · Functions

How do I write a generic function in JavaScript with @template?

  • Node.js (ES modules)
  • @template
  • Generics
  • keyof
  • Checked with TypeScript 5.9

The problem I have small helpers like last(), indexById() and pluck() that work on any array. With @param {any[]} they lose the element type. How do I make them generic in JSDoc?

The short answer

Declare a type parameter with @template T and use T in the @param and @returns types. Put a constraint in braces before the name, and use keyof for a parameter that must be one of the object’s keys:

collections.js collections.ts TypeScript 5.9.3 · checked 21 October 2026
/**
 * @template T
 * @param {T[]} items
 * @returns {T | undefined}
 */
export function last(items) {
  return items[items.length - 1];
}

/**
 * @template {{ id: string }} T
 * @param {T[]} items
 * @returns {Map<string, T>}
 */
export function indexById(items) {
  return new Map(items.map((item) => [item.id, item]));
}

/**
 * @template T
 * @template {keyof T} K
 * @param {T[]} items
 * @param {K} key
 * @returns {T[K][]}
 */
export function pluck(items, key) {
  return items.map((item) => item[key]);
}

const orders = [
  { id: "A-1001", total: 40, status: "paid" },
  { id: "A-1002", total: 12, status: "open" },
];
const newest = last(orders);
const byId = indexById(orders);
const totals = pluck(orders, "total");
console.log(newest?.status, byId.get("A-1001")?.total, totals);
export function last<T>(items: T[]): T | undefined {
  return items[items.length - 1];
}

export function indexById<T extends { id: string }>(items: T[]): Map<string, T> {
  return new Map(items.map((item) => [item.id, item]));
}

export function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((item) => item[key]);
}

const orders = [
  { id: "A-1001", total: 40, status: "paid" },
  { id: "A-1002", total: 12, status: "open" },
];
const newest = last(orders);
const byId = indexById(orders);
const totals = pluck(orders, "total");
console.log(newest?.status, byId.get("A-1001")?.total, totals);
Generic helpers with @template.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

last(orders) returns the order type itself, indexById returns a Map of orders, and pluck(orders, "total") returns number[], all inferred from the arguments.

Why it works

@template T is the JSDoc spelling of <T>. TypeScript infers T at each call from the arguments, the same way it does for a .ts function, so the helper keeps whatever type it is given.

  • @template {{ id: string }} T constrains T: it can be any object type with a string id. Inside the function that is why item.id is allowed.
  • @template {keyof T} K declares a second parameter that must be one of T’s keys. T[K] is the type of that property (indexed access), so pluck returns the right element type for any key.

The same thing in TypeScript

The TypeScript equivalent tab writes the same functions with <T>, <T extends { id: string }> and <T, K extends keyof T>. In JSDoc the constraint goes in braces in front of the name; in TypeScript it follows extends.

What goes wrong

The constraints are checked at every call. Products have a sku, not an id, and "prize" is not one of their keys; both calls fail with TS2345:

collections.js Error confirmed · TypeScript 5.9.3 · checked 21 October 2026
/**
 * @template {{ id: string }} T
 * @param {T[]} items
 * @returns {Map<string, T>}
 */
export function indexById(items) {
  return new Map(items.map((item) => [item.id, item]));
}

/**
 * @template T
 * @template {keyof T} K
 * @param {T[]} items
 * @param {K} key
 * @returns {T[K][]}
 */
export function pluck(items, key) {
  return items.map((item) => item[key]);
}

const products = [{ sku: "LAMP-01", price: 3900 }];
indexById(products);error TS2345: Argument of type '{ sku: string; price: number; }[]' is not assignable to parameter of type '{ id: string; }[]'.
  Property 'id' is missing in type '{ sku: string; price: number; }' but required in type '{ id: string; }'.
pluck(products, "prize");error TS2345: Argument of type '"prize"' is not assignable to parameter of type '"sku" | "price"'.
Arguments that break the constraint.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • No type arguments at the call. first<string>(x) in a .js file is two comparisons, not a type argument; let inference work, or annotate the value you pass.
  • One @template line can declare several names. @template T, U works, but a constraint applies to one name only, so constrained parameters get a line each.
  • Classes too. @template T above a class makes T available in every member’s JSDoc, and new Queue() infers it from the constructor’s arguments (see generic classes).

Questions

How do I pass a type argument explicitly, like last<string>(items)?

You cannot in a .js file: last<string>(items) is parsed as two comparisons. Let tsc infer T from the arguments, or type the argument itself (/** @type {string[]} */ ([])) so the inference has something to work with.

What is T when there is nothing to infer it from?

In a .js file a type parameter with nothing to infer from becomes any (in a .ts file it is unknown). Give it a default with @template [T=string] when that matters.

Sources

Last reviewed 21 October 2026.