open atlas
↑ Back to track
TypeScript type system, deep TS · 02 · 05

keyof and indexed access types

keyof gives the union of a type's keys; T[K] indexes into a type. Index with a union of keys to get a union of value types, and Arr[number] to get an array's element type.

TS Middle ◷ 14 min
Level
FoundationsJuniorMiddleSenior

You define a User interface, then hand-write a second type UserKey = "id" | "name" | "email" for a function that picks a field. A teammate adds role to User and ships. Three months later a runtime bug: a config form lets users edit role, but a validation helper keyed off the stale UserKey silently skips it. The two definitions drifted because they were written twice. Index access types delete the duplicate: keyof User is the key union, derived automatically, and User[K] is the value type. Define the data once; let the types follow.

keyof: the union of a type’s keys

keyof T produces a union of the literal types of T’s property keys:

type User = { id: string; name: string; age: number };

type UserKey = keyof User;
//   ^? type UserKey = "id" | "name" | "age"

This union is derived — add a property to User and UserKey updates automatically. It is the building block for any operation that must iterate or constrain over a type’s own keys.

T[K]: indexed access

T[K] looks up the type stored at key K in T. The key must be a valid key of T (or you get an error):

type Name = User["name"];
//   ^? type Name = string
type Age = User["age"];
//   ^? type Age = number

type Bad = User["nope"];
// Error: Property 'nope' does not exist on type 'User'.

Indexing is by type, not by value: User["name"] is using the string literal type "name" as the index, not a runtime string. This is why it works on types that have no runtime existence.

Indexing with a union of keys → union of value types

When you need to say “the value at any of these keys”, you do not write a separate union by hand — you index by the key union and let the checker derive it. This is where keyof and T[K] compose into a real single-source-of-truth tool.

Index with a union of keys and you get the union of the corresponding value types:

type IdOrAge = User["id" | "age"];
//   ^? type IdOrAge = string | number

type AllValues = User[keyof User];
//   ^? type AllValues = string | number   ("id"→string, "name"→string, "age"→number)

T[keyof T] — “index by every key” — is the idiom for “the union of all value types in T”. Duplicate value types collapse (string | string | number = string | number). This is a frequent senior tool: deriving an allowed-values union from a config object, or the payload-type union from a discriminated union’s members.

type Action =
  | { type: "add"; n: number }
  | { type: "clear" };

type ActionType = Action["type"];
//   ^? type ActionType = "add" | "clear"   — the tag union, derived from the variants

Notice this works straight on a union type: Action["type"] reads type from each member and unions the results. That keeps the discriminant tags from the previous lesson as a single source of truth.

number-indexing arrays and tuples → element type

Arrays and tuples are indexed by number. So T[number] is the element type:

type Names = string[];
type Element = Names[number];
//   ^? type Element = string

const roles = ["admin", "editor", "viewer"] as const;
type Role = typeof roles[number];
//   ^? type Role = "admin" | "editor" | "viewer"

The as const + [number] combo is the canonical way to derive a literal union from a runtime array, so the array of values and its type stay in lockstep. For tuples, [number] gives the union of all positions, while a literal index gives one slot:

type Pair = [string, number];
type First = Pair[0];
//   ^? type First = string
type Either = Pair[number];
//   ^? type Either = string | number
Complete the analogy

keyof is like asking a form which fields it has; T[K] is like asking what kind of value field K holds. Deriving types this way keeps one ___ of truth instead of two definitions that drift.

Pitfall: keyof on index-signature types

When a type has an index signature, keyof returns the signature key type, not a finite literal union:

type Dict = { [key: string]: number };
type K1 = keyof Dict;
//   ^? type K1 = string | number
//      (string index signatures also admit number keys, because JS coerces numeric keys to strings)

type NumDict = { [key: number]: boolean };
type K2 = keyof NumDict;
//   ^? type K2 = number

This surprises people: keyof a string-indexed type is string | number, not string, because a numeric property access like dict[0] is legal (JS turns it into dict["0"]). If you later try to constrain a generic K extends keyof Dict expecting precise keys, you instead get the broad string | number and lose the precision you wanted. Prefer a concrete object type (or Record<SpecificUnion, V>) when you need a finite key union.

Edge cases

Indexing with a literal versus a union changes precision dramatically. User["id"] is a single value type (string). User[keyof User] is the whole value-type union. And User[string] is an error unless User has a string index signature — indexing requires the index type to be assignable to a key of the target. This is why obj[someStringVariable] often errors on a precisely-typed object: a runtime string is wider than the object’s finite key union, so the access is unsound. Narrow the key to keyof T first.

Why this bridges to mapped types

Indexed access answers “what type is at this key”. The natural next question — “transform every key” — is mapped types (unit 04): { [K in keyof T]: ... } iterates the keyof T union and uses T[K] to read each value type. Everything you do with mapped types is built on the two operators in this lesson, so internalising keyof and T[K] now pays off directly there.

Derive instead of duplicate

1/3
Quiz

Given `type T = { a: string; b: number; c: string }`, what is `T[keyof T]`?

Quiz

You have `const perms = ['read','write'] as const`. Which type gives you the union `'read' | 'write'`?

Recall before you leave
  1. 01
    What do keyof T and T[K] each produce, and why is indexing 'by type, not by value'?
  2. 02
    What does T[keyof T] give, and how does indexed access behave on a union and on arrays?
  3. 03
    Why does keyof on an index-signature type surprise you, and when should you avoid it?
Recap

Index access types close the unit by turning the union machinery into a way to derive types from data. keyof T gives the union of a type’s keys and T[K] reads the type at a key; indexing by a union of keys yields the union of value types, so T[keyof T] is every value type and Action["type"] extracts a discriminant tag union straight from the variants you modelled last lesson. Arrays index by number, making Arr[number] the element type and typeof arr[number] (with as const) a literal union pulled from a runtime array — the antidote to hand-written, drift-prone duplicate unions. The one pitfall to remember is that keyof on a string index signature is string | number, not string. These two operators — keyof and T[K] — are exactly the primitives that mapped types in unit 04 use to transform every key of a type at once, so the derivation skills you built here carry directly into the type-system-deep unit. Now when you see a hand-maintained key union drifting from its object, you will reach for keyof typeof instead of patching the duplicate.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.