Recipe · Objects and data

How do I type a Map or a Set in JSDoc?

  • Node.js (ES modules)
  • Map
  • Set
  • @type
  • Checked with TypeScript 5.9

The problem I keep products in a Map keyed by SKU, but tsc lets me put anything in it, and when I add a type it complains that get() might be undefined. How do I type Maps and Sets properly in JSDoc?

The short answer

Put a @type with the key and value types on the declaration of an empty Map or Set:

inventory.js inventory.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
/**
 * @typedef {object} Product
 * @property {string} sku
 * @property {number} price
 */

/** @type {Map<string, Product>} */
const bySku = new Map();

/** @type {Set<string>} */
const discontinued = new Set();

/** @param {Product} product */
export function addProduct(product) {
  bySku.set(product.sku, product);
}

/**
 * @param {string} sku
 * @returns {number | undefined}
 */
export function priceOf(sku) {
  if (discontinued.has(sku)) return undefined;
  return bySku.get(sku)?.price;
}

const restock = new Map([
  ["LAMP-01", 4],
  ["DESK-02", 1],
]);
for (const [sku, quantity] of restock) console.log(sku.toLowerCase(), quantity.toFixed(0));
interface Product {
  sku: string;
  price: number;
}

const bySku = new Map<string, Product>();

const discontinued = new Set<string>();

export function addProduct(product: Product): void {
  bySku.set(product.sku, product);
}

export function priceOf(sku: string): number | undefined {
  if (discontinued.has(sku)) return undefined;
  return bySku.get(sku)?.price;
}

const restock = new Map([
  ["LAMP-01", 4],
  ["DESK-02", 1],
]);
for (const [sku, quantity] of restock) console.log(sku.toLowerCase(), quantity.toFixed(0));
A Map and a Set with their types on the declaration.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

bySku.set now accepts only a string and a Product, and bySku.get(sku) returns Product | undefined, which the optional chaining in priceOf handles. The restock Map needs no JSDoc, because its initial entries say what it holds.

Why it works

Map<K, V> and Set<T> are generic classes in TypeScript’s library. A @type on the variable fixes K and V, and every method is checked against them. get returns V | undefined because the key may be missing, and has does not change that: if (bySku.has(sku)) bySku.get(sku) is still possibly undefined to tsc.

The same thing in TypeScript

The TypeScript equivalent tab passes type arguments to the constructor, new Map<string, Product>(). JavaScript has no place for type arguments at a call, so the type goes on the variable instead.

What goes wrong

With the types in place, a key of the wrong type fails (TS2345), and reading a property straight off get() fails because the result may be undefined (TS2532):

inventory.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: inventory.js(7,11): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. inventory.js(9,13): error TS2532: Object is possibly 'undefined'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. inventory.js(7,11): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. inventory.js(9,13): error TS2532: Object is possibly 'undefined'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. inventory.js(7,11): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. inventory.js(9,13): error TS2532: Object is possibly 'undefined'. checked 21 October 2026
/** @typedef {{ sku: string, price: number }} Product */

/** @type {Map<string, Product>} */
const bySku = new Map();

bySku.set(1001, { sku: "LAMP-01", price: 3900 });error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
console.log(bySku.get("LAMP-01").price);error TS2532: Object is possibly 'undefined'.
A key of the wrong type, and get() that may find nothing.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Without the @type, an empty new Map() holds any, and none of this is reported, even under strict:

inventory.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
const bySku = new Map();

bySku.set(1001, "not a product");
const product = bySku.get("LAMP-01");
console.log(product.sku.toFixed(2));
An untyped new Map() holds any, so none of this is reported.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • has() does not narrow get(). Read once and check the result instead: const product = bySku.get(sku); if (product) ….
  • An empty new Set() is Set<any> too. It accepts numbers and strings side by side without a word; give it a @type like the Map.
  • Object keys are compared by identity. A Map<{ id: string }, …> finds an entry only with the same object, not with an equal one; the types cannot help with that.

Questions

When does tsc infer the types of a Map by itself?

When the constructor gets initial entries: new Map([["LAMP-01", 4]]) is a Map<string, number>. An empty new Map() has nothing to infer from, so in a .js file it is Map<any, any>.

Map or plain object for a lookup table?

A Map keeps insertion order, allows any key type and has a cheap size; a plain object typed with Record is easier to write as a literal and to serialize. Both are fully typed.

Sources

Last reviewed 21 October 2026.