skip to content

In TypeScript, given `const STATUS = { idle: 'idle', busy: 'busy' };`, how do you derive a union type of its values, and what has to be true of the object for that to work?

level: middleimportance: must knowfreq 60%

answer

  1. one declaration, two worlds
  2. typeof crosses value to type
  3. keyof for keys, indexed access for values
  4. arrays index with number
  5. widened values give you string

basics

~20 s

Assert the object with as const so its values keep their literal types, then take an indexed access over its keys: type Status = (typeof STATUS)[keyof typeof STATUS], which is "idle" | "busy". Without as const the values widen and the union collapses to string.

solid answer

~40 s

You bridge from the value world to the type world with `typeof`, then index into it. `keyof typeof STATUS` gives the union of keys, and `(typeof STATUS)[keyof typeof STATUS]` gives the union of the value types. The catch is that this only produces something useful if the values are literal types — so the object must be declared `as const`. Without the assertion the properties widen to `string`, and the derived union is just `string`, which is worthless as a constraint. For an array the same idea uses a numeric index: with `const ROLES = ['admin', 'user'] as const`, `(typeof ROLES)[number]` is `"admin" | "user"`. The payoff is a single source of truth: the runtime object and the type are the same declaration, so adding a key updates the union automatically.

code

typescript · 17 lines
typescript
const STATUS = {
  idle: 'idle',
  busy: 'busy',
} as const;

type StatusKey = keyof typeof STATUS;                // "idle" | "busy"
type Status = (typeof STATUS)[keyof typeof STATUS];  // "idle" | "busy"

function render(s: Status): string {
  return s === 'idle' ? 'Idle' : 'Busy';
}

const label = render(STATUS.busy);

const ROLES = ['admin', 'user'] as const;
type Role = (typeof ROLES)[number];                  // "admin" | "user"
const everyRole: readonly Role[] = ROLES;

go deeper

for a junior

Know that a union of allowed values can be derived from a constants object rather than typed out twice, and that the object needs as const for it to work.

for a middle

Be ready to write the derivation on a whiteboard and explain each piece: the type query typeof, keyof for the key union, and the indexed access for the value union, plus why widening breaks it.

for a senior

Argue for the single-source-of-truth benefit and know its limits — runtime APIs like Object.keys still return string[], so the precision has to be re-asserted at those boundaries.

for a principal

Decide where derived unions belong in a shared codebase: which module owns the table, whether consumers import the type or the value, and how much inference-heavy typing a team can maintain before errors become unreadable.

## The problem this solves You often have a table of constants that exists at runtime — for logging, for iteration, for sending over the wire — and you also want a type that says "one of these values". Writing both by hand means two declarations that drift apart. Deriving the type from the value keeps one source of truth. ## Step 1: cross into the type world with `typeof` In a *type* position, `typeof x` is the **type query** operator: it asks the checker for the type of the value binding `x`. This is a compile-time operation and has nothing to do with the runtime `typeof` operator that returns a string. ```ts const STATUS = { idle: 'idle', busy: 'busy' }; type StatusObj = typeof STATUS; // { idle: string; busy: string } ``` Notice what you get: `string`, not the literals. An object literal's properties are mutable, so their fresh literal types are widened. That widening is exactly what defeats the derivation. ## Step 2: pin the values with `as const` ```ts const STATUS = { idle: 'idle', busy: 'busy' } as const; type StatusObj = typeof STATUS; // { readonly idle: "idle"; readonly busy: "busy" } ``` Now the property types are the literals themselves, and everything downstream becomes precise. ## Step 3: index into it Two operators do the work: - `keyof T` produces the union of `T`'s property keys. - `T[K]` — an **indexed access type** — produces the type stored at key `K`; when `K` is a union, the result is the union of all those property types. ```ts type StatusKey = keyof typeof STATUS; // "idle" | "busy" type Status = (typeof STATUS)[keyof typeof STATUS]; // "idle" | "busy" ``` Here the keys and values happen to coincide; in a table like `{ ok: 200, notFound: 404 } as const` they clearly do not — `keyof` gives `"ok" | "notFound"` while the indexed access gives `200 | 404`. Choose whichever side of the table is the domain you care about. The parentheses around `typeof STATUS` are optional — the type query binds to the identifier before the index is applied, so `typeof STATUS[keyof typeof STATUS]` means the same thing — but writing them makes the intent obvious to a reader. ## Arrays: index with `number` For a const-asserted array literal, the element union comes from indexing with `number`, because every numeric index of the tuple contributes its element type: ```ts const ROLES = ['admin', 'user'] as const; type Role = (typeof ROLES)[number]; // "admin" | "user" function grant(role: Role) { /* ... */ } grant(ROLES[1]); // ok grant('root'); // rejected: not in the union ``` This is the standard way to get a list you can iterate at runtime *and* a union you can check against, from one declaration. ## Why widening ruins it Drop the assertion and follow the same steps: `typeof ROLES` is `string[]`, so `(typeof ROLES)[number]` is `string`, and `grant('root')` compiles. The derivation still "works" mechanically — it just yields a type that constrains nothing. This is the single most common failure mode, and the reason `as const` and this idiom are usually taught together. ## Practical notes - The derived type is computed at compile time and disappears at emit; there is no runtime helper involved and no cost. - Because the object is `readonly` under the assertion, consumers cannot mutate the table — which is usually what you want for a constant. - If you want the values to be checked against a shape *and* stay literal, that is a separate tool (`satisfies`) and a separate question; the plain derivation above places no constraint on what the values are. - Keep the table exported from one module. Every consumer then imports both the value and the derived type, and adding an entry updates both at once. ## A worked shape ```ts const HTTP = { ok: 200, created: 201, notFound: 404 } as const; type HttpName = keyof typeof HTTP; // "ok" | "created" | "notFound" type HttpCode = (typeof HTTP)[keyof typeof HTTP]; // 200 | 201 | 404 function describe(code: HttpCode): HttpName | 'unknown' { const hit = (Object.keys(HTTP) as HttpName[]).find((k) => HTTP[k] === code); return hit ?? 'unknown'; } ``` The assertion on `Object.keys` is needed because that function is typed to return `string[]` regardless of the object — a reminder that the precision lives in the type layer, not in the runtime API.

  • What does the derivation produce if you forget the `as const`?
    It still compiles, but the union collapses to the widened base type — `string` for a table of strings, `number` for status codes. The type constrains nothing, so bad values slip through. That silent degradation is why the idiom is always paired with the assertion.
  • How would you get the union of element types from a const-asserted array instead of an object?
    Index the tuple type with `number`: for `const ROLES = ['admin', 'user'] as const`, `(typeof ROLES)[number]` is `"admin" | "user"`. Every numeric position contributes its element type, so the indexed access collects them all into one union.
  • Why is `typeof` in `keyof typeof STATUS` not the same `typeof` you write in ordinary JavaScript code?
    In a type position it is the type query operator, which asks the checker for the declared type of a value binding and yields a type. The runtime operator yields a string at execution time. They share a keyword and nothing else — the type query is erased at emit.

saying these in an interview costs you the question

  • Derives the union without as const and gets string
  • Confuses the type query typeof with the runtime operator
  • Thinks keyof gives values rather than keys
  • Uses (typeof arr)[0] expecting the whole element union
  • Maintains the union and the object as two hand-written declarations

context