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?
answer
- one declaration, two worlds
- typeof crosses value to type
- keyof for keys, indexed access for values
- arrays index with number
- widened values give you string
basics
~20 sAssert 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 sYou 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 linesconst 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
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.
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.
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.
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