Skip to Content
const Assertions for a Fixed Lookup Table
Advanced
0 likes
Tutorials

const Assertions for a Fixed Lookup Table

Freeze a lookup table with as const and derive its keys and values as types. After this, the table and the type that describes it can never drift apart.

Skip ahead: If you already use as const and only need the lookup helper, skip to "Step 3: a lookup that cannot miss", then continue.

A lookup table of status codes, plan names or roles is one of the most common things in an application, and it is where as const pays for itself. The values are fixed, you want them as exact types, and you want one list to feed the type, the lookup and the loop. This page builds one from scratch and ends with a typed lookup function.

Step 1: the table as written

const LABELS = { 200: "OK", 404: "Not Found", 500: "Server Error" };

function describe(code: 200 | 404 | 500): string {
  return LABELS[code];
}

const label: "OK" = LABELS[200];
plain_table.ts(7,7): error TS2322: Type 'string' is not assignable to type '"OK"'.

Without as const the values widen to string, so the compiler cannot tell "OK" from any other text. The describe function works because the numeric keys are known, but the labels have lost their exact types, and the union of codes is typed by hand in the parameter, a second place to keep in sync.

Step 2: freeze it with as const

const LABELS = {
  200: "OK",
  404: "Not Found",
  500: "Server Error",
} as const;

type Code = keyof typeof LABELS;
type Label = (typeof LABELS)[Code];

const okCode: Code = 200;
const badCode: Code = 201;
const wrongLabel: Label = "Teapot";
LABELS[404] = "Missing";
const_table.ts(11,7): error TS2322: Type '201' is not assignable to type '200 | 404 | 500'.
const_table.ts(12,7): error TS2322: Type '"Teapot"' is not assignable to type 'Label'.
const_table.ts(13,8): error TS2540: Cannot assign to '404' because it is a read-only property.

Three things came from one object. Code is 200 | 404 | 500, built from the keys. Label is the union of the three exact strings. And the table is readonly, so the assignment on the last line fails. Add a row, and both types grow.

Step 3: a lookup that cannot miss

const LABELS = {
  200: "OK",
  404: "Not Found",
  500: "Server Error",
} as const;

type Code = keyof typeof LABELS;

function describe(code: Code): (typeof LABELS)[Code] {
  return LABELS[code];
}

function isCode(value: number): value is Code {
  return value in LABELS;
}

for (const status of [200, 404, 418]) {
  console.log(status, isCode(status) ? describe(status) : "unknown status");
}
200 OK
404 Not Found
418 unknown status

describe takes only known codes. A number from outside, like a response status, has to pass isCode first, which narrows it. The mistake you cannot make is calling describe(418) without checking: the compiler stops you.

const LABELS = { 200: "OK", 404: "Not Found", 500: "Server Error" } as const;
type Code = keyof typeof LABELS;
declare function describe(code: Code): string;

describe(418);
lookup_error.ts(5,10): error TS2345: Argument of type '418' is not assignable to parameter of type '200 | 404 | 500'.

Step 4: a list of values, with the same trick

For a plain list, such as the roles a user can have, as const turns an array into a readonly tuple and gives you the union with one index type.

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number];

function isRole(value: string): value is Role {
  return (ROLES as readonly string[]).includes(value);
}

console.log(ROLES.length, isRole("editor"), isRole("owner"));
3 true false

(typeof ROLES)[number] means "the type you get when you index the tuple with any number", which is the union of its elements. The cast to readonly string[] is needed because includes on a tuple of literals only accepts those literals. Keep it inside the guard, where it belongs.

Iterating the table

Object.keys and Object.entries do not keep your literal types, because an object can have more keys at run time than its type says.

const LABELS = { 200: "OK", 404: "Not Found" } as const;

for (const key of Object.keys(LABELS)) {
  console.log(LABELS[key]);
}
iterate.ts(4,15): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ readonly 200: "OK"; readonly 404: "Not Found"; }'.
  No index signature with a parameter of type 'string' was found on type '{ readonly 200: "OK"; readonly 404: "Not Found"; }'.

When you know the object has no extra keys, as here, cast once: (Object.keys(LABELS) as Code[]). Do it in one helper, not at every loop.

⚠ Warning: as const spreads: it makes everything below it readonly. A function that takes a plain string[] will refuse readonly ["admin", "editor", "viewer"]. Change the parameter to readonly string[], which is the honest type for a function that does not modify its input.

Common mistakes

  • Putting as const on a variable instead of a literal. It only works on the literal expression.
  • Keeping the union type and the array in two places. If you write type Role = "admin" | "editor" next to the array, you can forget to update one.
  • Using an enum where a frozen object is simpler. An enum also produces run-time code and has surprising reverse mappings.

Related course: TypeScript Advanced.

About

Freeze a lookup table with as const and derive its keys and values as types. After this, the table and the type that describes it can never drift apart.

Responsible
Jay P
Last Update
10/10/2026
Reading Time
5 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.