In TypeScript, given `interface User { id: number; name?: string }` and `const roles = ['admin', 'user'] as const`, what types do the indexed access types `User['id']`, `User[keyof User]` and `(typeof roles)[number]` produce?
answer
- read a property's type by key
- brackets, but in type position
- keyof as the index gives every value type
- number is the index for arrays and tuples
- optional keys carry undefined along
basics
~20 sIndexed access reads a property's type by key: User['id'] is number, User[keyof User] is the union of all property types (number | string | undefined), and (typeof roles)[number] is the union of the tuple's elements, 'admin' | 'user'.
solid answer
~40 sBracket syntax in *type* position is an indexed access type — it looks up a property's type by key. `User['id']` is `number`. `User[keyof User]` uses the whole key union as the index, so it returns the union of every property type: `number | string | undefined`, with the `undefined` coming from `name?` under `strictNullChecks`. Indexing with a union distributes, so `User['id' | 'name']` is `number | string | undefined` too. For arrays and tuples the index is `number`: `(typeof roles)[number]` on a `readonly ['admin', 'user']` gives `'admin' | 'user'`, which is the standard way to derive a union of literals from a `const`-asserted array. Indexing with a key the type does not have is a compile error, not `undefined` — the checker rejects the lookup outright.
code
typescript · 15 linesinterface User {
id: number;
name?: string;
}
type Id = User["id"]; // number
type Name = User["name"]; // string | undefined
type AnyValue = User[keyof User]; // number | string | undefined
const roles = ["admin", "user"] as const;
type Role = (typeof roles)[number]; // "admin" | "user"
type Pair = [number, string];
type First = Pair[0]; // number
type Either = Pair[number]; // number | stringgo deeper
Recognise that square brackets in a type mean a type lookup, and be able to say that User['id'] is the type of that property rather than its value.
Explain that a union index distributes, that T[keyof T] yields the union of all property types, and that number is the index that reaches array and tuple elements.
Show the as const plus (typeof arr)[number] pattern as a single source of truth, and explain why the compile error on a missing key is the property you actually want from it.
Argue for deriving types from data rather than declaring both: it removes a class of drift between runtime constants and their types, at the cost of types that are harder to read in tooling. Say where you draw that line.
## The syntax In type position, `T[K]` is an **indexed access type**: it asks the checker for the type of `T`'s property named `K`. It mirrors the runtime property-access syntax deliberately, but it operates on types and, like every other type construct, disappears at emit. ```ts interface User { id: number; name?: string; } type Id = User["id"]; // number ``` The index must itself be a *type*, not a value: `User[someStringVariable]` is meaningless. It is normally a string literal type, a union of them, `number`, or `keyof T`. ## Indexing with a union distributes Give it several keys at once and you get the union of their types back: ```ts type IdOrName = User["id" | "name"]; // number | string | undefined ``` The interesting case is indexing with the type's *entire* key set: ```ts type AnyValue = User[keyof User]; // number | string | undefined ``` `keyof User` is `"id" | "name"`, so `User[keyof User]` collapses to the union of all property types. This "value union" is the counterpart of the key union and is exactly how you derive, say, the union of allowed values from a lookup object. ## Optional properties bring `undefined` With `strictNullChecks` on, an optional property's type includes `undefined`, and indexed access reports that faithfully: ```ts type Name = User["name"]; // string | undefined ``` This surprises people who expect the `?` to be "stripped" by the lookup. It is not — the lookup returns the declared property type, and `name?: string` declares `string | undefined`. If you want the non-optional form you have to remove `undefined` explicitly. ## Missing keys are an error, not `undefined` ```ts type Nope = User["nope"]; // error: cannot be used to index User ``` At runtime, reading a missing property yields `undefined`; in the type layer, indexing with a key that is not in the key set is a compile error. That difference is the point of the construct — it is a checked lookup, so a rename of the property breaks every derived type that referenced the old name, which is precisely the refactor safety you want. ## Arrays and tuples index with `number` Arrays and tuples have numeric keys, so the element type is read with a `number` index: ```ts type El = string[][number]; // string type Pair = [number, string]; type First = Pair[0]; // number type Either = Pair[number]; // number | string ``` A tuple can be indexed with a specific numeric literal (`Pair[0]`) to reach one position, or with `number` to get the union of all positions. The most common practical use combines this with a `const` assertion: ```ts const roles = ["admin", "user"] as const; type Role = (typeof roles)[number]; // "admin" | "user" ``` `as const` makes the array a `readonly ["admin", "user"]` tuple of literal types; `typeof roles` queries that type; indexing it with `number` unions the element types. The result is a single source of truth: one runtime array that you can iterate, plus a type derived from it that cannot drift out of sync. Adding a role to the array widens the type automatically. Note the parentheses — writing `(typeof roles)[number]` makes the grouping explicit and is the form most style guides prefer. ## Chaining Indexed access composes, which is how you reach into nested shapes: ```ts interface Config { server: { host: string; port: number }; } type Port = Config["server"]["port"]; // number ``` Each step is checked, so a nested rename surfaces immediately. ## Why it matters Indexed access is the *read* half of a pair. `keyof` gives you the set of keys; `T[K]` turns a key back into the type stored there. Together they let you describe a type in terms of another type rather than restating it, and they are the two primitives a mapped type uses when it walks a type property by property — the key set on the left of the iteration, the indexed access on the right. Everything erases: `T[K]` costs nothing at runtime and produces no code, it only constrains what the compiler will accept.
- What happens if you index with a key the type does not declare, such as `User['nope']`?It is a compile error — the checker reports that the key cannot be used to index the type. It does not silently return `undefined` the way a runtime property read would. That strictness is the feature: renaming or removing a property immediately breaks every derived type that still names the old key.
- How would you derive the union of values from a `const`-asserted lookup object?Index the queried type with its own key union. With `const STATUS = { ok: 200, missing: 404 } as const`, the type `(typeof STATUS)[keyof typeof STATUS]` is `200 | 404`. The `as const` keeps the values as literal types instead of widening them to `number`, and `keyof typeof STATUS` supplies every key so the lookup unions all of them.
- Why does `User['name']` include `undefined` when `name?: string` is declared?Because under `strictNullChecks` the optional modifier makes the declared property type `string | undefined`, and indexed access returns the declared type verbatim. The lookup does not strip optionality. If you need the bare `string`, you have to remove `undefined` from the union yourself rather than expecting the index to do it.
saying these in an interview costs you the question
- Expects an unknown key to yield undefined instead of an error
- Thinks T[K] runs at runtime like property access
- Forgets optional properties contribute undefined
- Uses a value instead of a type as the index
- Believes tuples can only be indexed by literal positions