Skip to Content
Record Keys That Match a Real Enum
Intermediate
0 likes
Tutorials

Record Keys That Match a Real Enum

Key a lookup table by an enum or a union of literals so a missing or misspelled key is a compile error. After this, adding a status shows you every table that needs an entry.

A lookup table keyed by status, role or size is one of the most common objects in an app: the label for each status, the color for each role. The risk is that somebody adds a new status and forgets the table. A plain object with string keys accepts that quietly. Record keyed by the enum does not.

Step 1: the enum

enum Status {
  Draft = "draft",
  Published = "published",
  Archived = "archived",
}

console.log(Status.Published);
console.log(Object.values(Status));
published
[ 'draft', 'published', 'archived' ]

A string enum gives each member a readable value, and it exists at run time as a real object. That is different from a type alias of literals, which disappears when compiled. Both work with Record.

Step 2: a table that must cover every member

enum Status {
  Draft = "draft",
  Published = "published",
  Archived = "archived",
}

const labels: Record<Status, string> = {
  [Status.Draft]: "Draft",
  [Status.Published]: "Live",
};
labels.ts(7,7): error TS2741: Property 'archived' is missing in type '{ draft: string; published: string; }' but required in type 'Record<Status, string>'.

Record<Status, string> means every member of Status is a key and every value is a string. The compiler names the member you left out. The keys are written as [Status.Draft], so renaming a member in the enum updates the table instead of leaving a stale word behind. Plain draft: also compiles, but it ties the table to the value and not to the member.

Step 3: add a member and see where it breaks

Add Scheduled = "scheduled" to the enum and complete the table.

enum Status {
  Draft = "draft",
  Published = "published",
  Scheduled = "scheduled",
  Archived = "archived",
}

const labels: Record<Status, string> = {
  [Status.Draft]: "Draft",
  [Status.Published]: "Live",
  [Status.Scheduled]: "Waiting",
  [Status.Archived]: "Hidden",
};

for (const status of Object.values(Status)) {
  console.log(status, "->", labels[status]);
}
draft -> Draft
published -> Live
scheduled -> Waiting
archived -> Hidden

Any other Record<Status, ...> in the project would now fail until it has a Scheduled entry. That is the point: the enum becomes a checklist.

Step 4: no enum? Use a union of literals

Many teams avoid enums and use a union instead. The table works the same way.

type Status = "draft" | "published" | "archived";

const labels: Record<Status, string> = {
  draft: "Draft",
  published: "Live",
  archive: "Hidden",
};
union.ts(6,3): error TS2561: Object literal may only specify known properties, but 'archive' does not exist in type 'Record<Status, string>'. Did you mean to write 'archived'?

The typo archive is reported as a key that does not exist, and the compiler even guesses what you meant. Fix it and the table is complete. A union costs nothing at run time. An enum gives you an object you can loop over. Pick by that, and not by fashion.

⚠ Warning: Record<string, string> accepts every key. If you wrote the type as { [key: string]: string } to save time, you have thrown the checklist away.

Common mistakes

  • Typing the table as Record<string, string> and losing the missing-key error.
  • Using a numeric enum, where Object.values also returns the reverse mapping names.
  • Writing the keys as plain words like draft: and then changing the enum value. Use the member in square brackets so the table follows the enum.

Related course: TypeScript Intermediate.

About

Key a lookup table by an enum or a union of literals so a missing or misspelled key is a compile error. After this, adding a status shows you every table that needs an entry.

Responsible
Christopher R
Last Update
10/17/2026
Reading Time
3 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.