Indexed Access Types

Advanced · 18 min read · ▶ live playground · ✦ checkpoint

An indexed access type reads the value type behind a key: User["email"] means "the type of the email property on User." Last lesson, keyof gave you the key set; now the bracket syntax reaches through one of those keys and gets the type on the other side.

Use it when a field type already exists inside a shape and you do not want to write that type twice.

Reach into one property

The syntax mirrors value access, but it happens entirely in the type layer:

type User = {
  email: string;
};
 
type UserEmail = User["email"];

A selected key is the key you put inside the brackets, written as a string literal type like "email". If keyof reads drawer labels, an indexed access type opens a selected drawer and reads the label on the contents.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

UserEmail is string. UserPlan is "free" | "pro". The type aliases did not invent new promises; they borrowed the existing property types from User.

That sounds small until a shape changes. If User["plan"] becomes "free" | "pro" | "team", UserPlan updates with it. If email changes from string to a reusable EmailAddress alias later, every derived email type follows the original field.

Unions open several drawers

The key inside the brackets does not have to be a single key. If you index with a key union, TypeScript returns a union of the matching value types.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

User["email" | "karma"] becomes string | number. Read it as "the value type at email, or the value type at karma."

You can combine this with keyof too:

type User = {
  id: string;
  email: string;
  plan: "free" | "pro";
  karma: number;
};
 
type UserValue = User[keyof User];

That means "all value types from all keys of User." It is powerful, but it can get wide fast. In the User shape above, UserValue is effectively string | number, because id, email, and plan are all string-shaped values and karma is a number.

number means any numbered slot

Arrays and tuples have numbered keys. When you index an array type with number, you get the element type, meaning the type of one item from that array.

That is the pattern behind Array[number]:

type LaunchArray = Array<{
  code: string;
  year: number;
  crew: number;
}>;
 
type Launch = LaunchArray[number];

It reads as "one item from LaunchArray." The array is the whole launch list; LaunchArray[number] is one launch record from any numbered position.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

The chain is doing three separate moves:

  • typeof launches takes a snapshot of the value's array type.
  • LaunchArray[number] gets the type of one numbered element.
  • Launch["code"] and Launch["status"] reach into that element type.

This is why Array[number] shows up so often next to typeof. You author a real list once, then derive "one item from this list" and "one field from one item" from the data.

Deep chains need judgment

A deep access chain is an indexed access type that walks through several nested properties, such as AppState["session"]["user"]["profile"]["email"]. It is legal, and sometimes it is exactly right.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

Notice the split. OwnerTeam uses one deep chain because it is still readable. The user email path gets named in steps: SessionUser, then UserProfile, then ProfileEmail.

That is the readability rule. If a chain makes you count brackets, name the middle. Type aliases are cheap, and good names tell the next reader which part of the shape matters.

Indexed access retrieves pieces from existing types. Next lesson adds a new move: conditional types, where the type layer chooses between pieces based on a check.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion