Type expression

Record<K, V> and Object.<K, V>

  • Collections
  • Tested with TypeScript 5.9

Record<K, V> is the type of an object whose keys are of type K and whose values are of type V. {Record<string, number>} fits a stock table keyed by SKU; {Record<"S" | "M" | "L", number>} requires exactly those three keys.

Two older spellings mean the same as Record<string, number>: the index signature {{ [sku: string]: number }} and the Closure form {Object.<string, number>}. TypeScript accepts all three; Record is the shortest and works with a union of keys.

At a glance

Use JSDoc in a .js file TypeScript in a .ts file
Any string key /** @type {Record<string, number>} */ : Record<string, number>
A fixed set of keys /** @type {Record<"S" | "M" | "L", number>} */ : Record<"S" | "M" | "L", number>
Index signature /** @type {{ [sku: string]: number }} */ : { [sku: string]: number }
Closure spelling /** @type {Object.<string, number>} */ : { [x: string]: number }

Examples

stock.js stock.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
/** @type {Record<string, number>} */
const stock = { "TEE-01": 12, "MUG-07": 3 };

stock["CAP-02"] = 8;

/** @param {Record<string, number>} levels */
export function lowStock(levels) {
  return Object.entries(levels)
    .filter(([, count]) => count < 5)
    .map(([sku]) => sku);
}

lowStock(stock);
const stock: Record<string, number> = { "TEE-01": 12, "MUG-07": 3 };

stock["CAP-02"] = 8;

export function lowStock(levels: Record<string, number>) {
  return Object.entries(levels)
    .filter(([, count]) => count < 5)
    .map(([sku]) => sku);
}

lowStock(stock);
Record<K, V> for an object used as a dictionary.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
sizes.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: sizes.js(5,7): error TS2741: Property 'L' is missing in type '{ S: number; M: number; }' but required in type 'Record<Size, number>'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. sizes.js(5,7): error TS2741: Property 'L' is missing in type '{ S: number; M: number; }' but required in type 'Record<Size, number>'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. sizes.js(5,7): error TS2741: Property 'L' is missing in type '{ S: number; M: number; }' but required in type 'Record<Size, number>'. checked 21 October 2026
/** @typedef {"S" | "M" | "L"} Size */

/** @type {Record<Size, number>} */
const stockBySize = { S: 4, M: 10 };error TS2741: Property 'L' is missing in type '{ S: number; M: number; }' but required in type 'Record<Size, number>'.

export { stockBySize };
A Record over a union of keys requires every key.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
stock.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: stock.js(13,1): error TS2322: Type 'string' is not assignable to type 'number'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. stock.js(13,1): error TS2322: Type 'string' is not assignable to type 'number'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. stock.js(13,1): error TS2322: Type 'string' is not assignable to type 'number'. checked 21 October 2026
/** @type {Object.<string, number>} */
const closureStyle = { "TEE-01": 12 };

/** @type {{ [sku: string]: number }} */
const indexSignature = { "TEE-01": 12 };

/** @type {Record<string, number>} */
const record = { "TEE-01": 12 };

closureStyle["MUG-07"] = 3;
indexSignature["MUG-07"] = 3;
record["MUG-07"] = "3";error TS2322: Type 'string' is not assignable to type 'number'.

export { closureStyle, indexSignature, record };
Object.<K, V> and index signatures mean the same thing.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
stock.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: stock.js(7,10): error TS2532: Object is possibly 'undefined'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. stock.js(7,10): error TS2532: Object is possibly 'undefined'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. stock.js(7,10): error TS2532: Object is possibly 'undefined'. checked 21 October 2026
/** @type {Record<string, number>} */
const stock = { "TEE-01": 12 };

/** @param {string} sku */
export function remaining(sku) {
  return stock[sku] - 1;error TS2532: Object is possibly 'undefined'.
}

/** @param {string} sku */
export function remainingOrZero(sku) {
  return (stock[sku] ?? 0) - 1;
}
With noUncheckedIndexedAccess, a lookup may be undefined.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

A union of keys

With literal keys, Record is complete or it is an error: leaving out L from a Record<Size, number> is TS2741, as the second example shows. That makes it a good fit for lookup tables that must cover every case of a union.

Lookups that may miss

By default, stock[sku] on a Record<string, number> is typed number, even for a key that is not there. With noUncheckedIndexedAccess, TypeScript adds undefined to the result and asks for a check (TS2532 in the fourth example), which matches what happens at run time.

For a dictionary that changes a lot, a Map<string, number> is often clearer: get() is always typed as possibly undefined, and keys are not mixed with object properties.

Gotchas

  • Record<string, V> claims every string key exists. Prefer Partial<Record<string, V>> or Map when that is not true and noUncheckedIndexedAccess is off.

Questions

How do I type an object with dynamic keys in JSDoc?

@type {Record<string, number>} for string keys and number values. {Object.<string, number>} and {{ [key: string]: number }} are equivalent spellings.

Sources

Last reviewed 20 October 2026.