Recipe · Objects and data

How do I type an object whose keys are a fixed set of strings?

  • Node.js (ES modules)
  • Record
  • Literal unions
  • Partial
  • Checked with TypeScript 5.9

The problem I keep a label for every order status in an object. When a new status is added, I want tsc to tell me the object is missing it. How do I type the keys as that union in JSDoc?

The short answer

Name the keys as a union of string literals, then type the object as Record<OrderStatus, string>. Use Partial<Record<…>> when only some keys need a value:

status.js status.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 {"draft" | "paid" | "shipped" | "cancelled"} OrderStatus */

/** @type {Record<OrderStatus, string>} */
const LABELS = {
  draft: "Draft",
  paid: "Paid",
  shipped: "On its way",
  cancelled: "Cancelled",
};

/** @type {Partial<Record<OrderStatus, string>>} */
const ICONS = { shipped: "truck", cancelled: "cross" };

/**
 * @param {OrderStatus} status
 * @returns {string}
 */
export function badge(status) {
  const icon = ICONS[status];
  return icon ? `${icon} ${LABELS[status]}` : LABELS[status];
}
type OrderStatus = "draft" | "paid" | "shipped" | "cancelled";

const LABELS: Record<OrderStatus, string> = {
  draft: "Draft",
  paid: "Paid",
  shipped: "On its way",
  cancelled: "Cancelled",
};

const ICONS: Partial<Record<OrderStatus, string>> = { shipped: "truck", cancelled: "cross" };

export function badge(status: OrderStatus): string {
  const icon = ICONS[status];
  return icon ? `${icon} ${LABELS[status]}` : LABELS[status];
}
A Record keyed by a union of strings must have every key.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

LABELS must have all four statuses; ICONS may have any of them. Reading LABELS[status] gives a string, and ICONS[status] gives string | undefined, which is why badge checks it.

Why it works

Record<K, V> is an object type with one property per member of K, each of type V. With a union of four strings for K, that is four required properties, and the object literal is checked against them like any other object type: nothing missing, nothing extra, every value a string. Partial makes each property optional, so its lookups include undefined.

The same thing in TypeScript

The TypeScript equivalent tab annotates const LABELS: Record<OrderStatus, string>. The type is identical; only the annotation moves from the JSDoc to the variable.

What goes wrong

Add "cancelled" to the union and forget the label, and the object fails where it is declared (TS2741). A key outside the union fails on its own line (TS2353):

status.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: status.js(5,7): error TS2741: Property 'cancelled' is missing in type '{ draft: string; paid: string; shipped: string; }' but required in type 'Record<OrderStatus, string>'. status.js(18,3): error TS2353: Object literal may only specify known properties, and 'refunded' does not exist in type 'Record<OrderStatus, string>'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. status.js(5,7): error TS2741: Property 'cancelled' is missing in type '{ draft: string; paid: string; shipped: string; }' but required in type 'Record<OrderStatus, string>'. status.js(18,3): error TS2353: Object literal may only specify known properties, and 'refunded' does not exist in type 'Record<OrderStatus, string>'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. status.js(5,7): error TS2741: Property 'cancelled' is missing in type '{ draft: string; paid: string; shipped: string; }' but required in type 'Record<OrderStatus, string>'. status.js(18,3): error TS2353: Object literal may only specify known properties, and 'refunded' does not exist in type 'Record<OrderStatus, string>'. checked 21 October 2026
/** @typedef {"draft" | "paid" | "shipped" | "cancelled"} OrderStatus */

/** @type {Record<OrderStatus, string>} */
const LABELS = {error TS2741: Property 'cancelled' is missing in type '{ draft: string; paid: string; shipped: string; }' but required in type 'Record<OrderStatus, string>'.
  draft: "Draft",
  paid: "Paid",
  shipped: "On its way",
};

/** @type {Record<OrderStatus, string>} */
const COLORS = {
  draft: "gray",
  paid: "green",
  shipped: "blue",
  cancelled: "red",
  refunded: "orange",error TS2353: Object literal may only specify known properties, and 'refunded' does not exist in type 'Record<OrderStatus, string>'.
};

export { LABELS, COLORS };
A missing key and a key that is not in the union.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • The error points at the declaration. TS2741 is reported on the variable, not inside the object; the message names the missing key.
  • Lookups with a plain string fail. LABELS[someString] is TS7053; the key must be an OrderStatus. See Object.keys for keys you get from the object itself.
  • Keep the union in one place. A const object and a @typedef of its values give you both the runtime list of statuses and the union for Record.

Questions

What is the difference between Record<OrderStatus, string> and Record<string, string>?

The first has exactly the keys in the union, all required. The second accepts any string key and requires none, so it cannot report a missing status.

Can I write the same type without Record?

Yes, as a mapped type: {{ [S in OrderStatus]: string }}. Record<K, V> is defined that way in TypeScript's library; Record is the shorter spelling.

Sources

Last reviewed 21 October 2026.