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.valuesalso 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
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.