Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~21 min· topic 3 of 5

Topic 8.3

Indexed Access Types

In one line

T["key"] looks up the type of one property inside another type, the same way obj["key"] looks up a value, and T[number] pulls out what an array holds.

Think of it like this

A shop has a price list pinned to the wall: a table with rows named "apple", "bread", "milk". You point at the row "milk" and read the number beside it. An indexed access type points at one row of a type and reads what is written there: Prices["milk"] is the type stored under milk.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Indexed access type
A type written T[K] that looks up the type of the property K in T.
Lookup
Reading one part out of a bigger thing, by name or position.
Union
A type that means "one of these", written with |.
Tuple
An array with a fixed length where each position has its own type.
Element type
The type of one item inside an array, found with T[number].
Optional property
A property that may be missing, written name?: string. Reading it can give undefined.
as const
A suffix that freezes a value into its exact literal types.
Index type
The type you put in the brackets, like "name", number or a union of keys.

Step by step

01One row of the price list

Given a User type, User["name"] is the type of its name. To go deeper, add more brackets. To get several at once, put a union in the brackets.

lookup.tswhole filets
type User = {
  id: number;
  name: string;
  address: { city: string; pin: string };
  nickname?: string;
};

type Name = User["name"];              // string
type City = User["address"]["city"];   // string
type IdOrName = User["id" | "name"];   // string | number

02Every field at once, and the optional trap

User[keyof User] unions every property type. Because nickname is optional, undefined is part of that union. For the same reason User["nickname"] is string | undefined.

03The element of an array

Order["items"] is an array type. Any array can be read with a number index, so adding [number] gives one element. A further ["price"] reads a field of that element.

element.tswhole filets
type Order = {
  id: string;
  items: { sku: string; price: number; qty: number }[];
};

type Item  = Order["items"][number];            // { sku: string; price: number; qty: number }
type Price = Order["items"][number]["price"];   // number
The element of an arraydiagram
Rendering diagram…

04Tuples and constant lists

In a tuple each slot has its own number: Pair[0], Pair[1]. Pair["length"] is the exact number 2. There is no slot 2, so Pair[2] is an error.

For a frozen list, (typeof colors)[number] is the union of its members, and you can still loop over colors at runtime.

05Generics: T[K]

In function readConfig<K extends keyof Config>(c: Config, key: K): Config[K] the return type changes with the key you pass. readConfig(cfg, "host") returns string and readConfig(cfg, "port") returns number. The same idea types a setter: value must match whatever key you chose.

06Types go in the brackets, not values

const key = "name"; type A = User[key]; is an error: key is a value. Write User[typeof key]. This is the same value-versus-type split as in Topic 8.2.

Try it yourself

  1. 1

    Predict the union

    In the first example write type Both = User["name" | "address"]; and predict the result before hovering. It's string | { city: string; pin: string }.

  2. 2

    Make an element optional

    In the second example change items to items?: { ... }[]. Now Order["items"][number] is an error (TS2537: Type '...[] | undefined' has no matching index signature for type 'number') and Item and Price become any: the array might be undefined, and you can't index undefined. Fix it with NonNullable<Order["items"]>[number] (NonNullable is in Topic 10.2).

  3. 3

    Frozen or loose

    In the second example delete as const from colors. Predict Color first (it becomes string), and what happens to the "pink" line.

Code & diagrams

T["key"]: look up one property's type

A union in the brackets gives a union of results. User[keyof User] collects every field type, and the optional nickname adds undefined. The last line is an error, because User has no age.

Console

(nothing was printed)
T[number]: the element type of an array

[number] strips the array and leaves one element. For the frozen colors list it gives the union of the three real colour names, which is why "pink" is rejected.

Console

(nothing was printed)

Expected console output

red
Tuples: pick a slot by number

Each slot of a tuple has a number. The length of a tuple is the exact literal 2, not just number, and a slot that doesn't exist is an error.

Console

(nothing was printed)
T[K] in a generic function and a typed setter

The return type depends on the key you pass. In Setter the value must match the key: a port must be a number. TypeScript warns about the last call, but the code runs and prints it anyway.

Console

(nothing was printed)

Expected console output

localhost 5432
set port 8080
set port 8080

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Look up a property that doesn't exist

Look at the last line of the first example: type Age = User["age"];. It fails. Change "age" to "id" and the error goes away.

terminal
$ Types panel
── what you'll see ──
index.ts(18,17): error TS2339: Property 'age' does not exist on type 'User'.

Break #2

Pass the wrong value to a keyed setter

In the last example the line set("port", "8080") is already wrong; change "8080" to 8080.

terminal
$ Types panel
── what you'll see ──
index.ts(17,13): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

Break #3

Use a variable as the index

At the end of the first example add two lines: const key = "name"; and type A = User[key];.

terminal
$ Types panel
── what you'll see ──
index.ts(20,15): error TS2538: Type 'key' cannot be used as an index type.
index.ts(20,15): error TS2749: 'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?

Myth vs fact

Myth

Order["items"][0] is how you get the element type of an array.

Fact

For arrays you use [number]. [0] on an ordinary array type is the same element type, but [number] is the form that also works for tuples of any length.

Myth

An optional property's lookup type is just the declared type.

Fact

Reading an optional property can give undefined, so User["nickname"] is string | undefined.

Myth

noUncheckedIndexedAccess changes Arr[number].

Fact

It only adds | undefined to expressions like arr[0] in your code. The type Arr[number] keeps the plain element type.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Inside generics, T[K] is a deferred type: the compiler can't compute it until T and K are known, so it reasons with rules instead. K extends keyof T is exactly what makes T[K] valid. Without the constraint you get Type 'K' cannot be used to index type 'T'.

  • ▸

    Indexing with a union distributes: T["a" | "b"] is T["a"] | T["b"]. Writing to a property through a union key is stricter: the value must satisfy the intersection of the property types, because the key could be any of them.

  • ▸

    Indexed access types are the building block under mapped types (Topic 8.4): { [K in keyof T]: T[K] } copies a type one lookup at a time.

Remember this

  1. 1

    The problem this solves: you need the type of one property, or of one array element, that already lives inside a bigger type, and you don't want to retype it. In JavaScript you read a property with square brackets: user["name"] gives the value. TypeScript uses the same brackets in the type world: User["name"] gives the type of the name property. This is an indexed access type. It lets you reuse a property's type without writing it a second time, so if name changes from string to a richer type, everything that points at it follows.

  2. 2

    You can chain lookups to go deep: User["address"]["city"] first finds the type of address, then the type of city inside it. Each step is checked, and a name that doesn't exist is an error (Property 'age' does not exist on type 'User'), so a typo can't slip through.

  3. 3

    The index can be a union of key names, and the result is the union of the property types: User["id" | "name"] is number | string. With keyof (Topic 8.1) you get every property type at once: User[keyof User] is the union of all the field types. If a property is optional (nickname?: string), User["nickname"] includes undefined, because reading it could give undefined.

  4. 4

    The most useful trick is T[number]. An array type like Item[] can be read with any number index, and Item[][number] is the type you get back: Item. So Order["items"][number] is "what one item of this order looks like". This is how you name the element type of an array that is buried inside a bigger type, without writing the element type out by hand or defining a separate type for it.

  5. 5

    It works on tuples (arrays with a fixed length and one type per position, Topic 5.2) too. Pair[0] is the type of the first slot, Pair[1] the second, Pair["length"] is the literal number 2. Asking for a slot that doesn't exist, like Pair[2], is an error. Pair[number] is the union of all slot types.

  6. 6

    Combine with typeof and as const to read the members of a constant list: const colors = ["red", "green", "blue"] as const; then (typeof colors)[number] is "red" | "green" | "blue". This is a classic way to get a union of allowed values from a list that also exists at runtime, so you can loop over the list and use the union as a type.

  7. 7

    Two rules to remember. First, the index must be a type, not a value: User[key] where key is a variable is an error, but User[typeof key] is fine. Second, in generics T[K] is the heart of safe property access (Topic 7.3); the compiler tracks that the result depends on which K you chose. Finally, noUncheckedIndexedAccess adds | undefined to *expressions* like arr[0], but not to the type Arr[0], which stays the plain element type.

Explain it without notes

01

What does T["key"] do, and how is it different from obj["key"]?

02

How do you get the type of one element of an array that is a property of another type?

03

What type does User["a" | "b"] produce, and what about User[keyof User]?

04

Why is User[key] an error when key is a const variable?

Practice

01

Given type Api = { users: { id: number; email: string }[]; total: number }, write type UserRow (the type of one user) and type Email.

02

Write a generic function function setField<T, K extends keyof T>(obj: T, key: K, value: T[K]): void and call it so that one call is accepted and one is rejected.

Trade-offs

  • ↔

    Indexed access keeps types in sync with the source, but long chains like A["b"][number]["c"] are hard to read. Give a chain a name with type once and use the name.

  • ↔

    Pulling a type out of a bigger type is convenient, but if you reuse a shape in many places, defining it once with a name (type OrderItem = {...}) and building Order from it is clearer.

Done when you can

  • I can write T["key"] and say what type it gives.

  • I can reach a nested type with chained brackets.

  • I can get an array's element type with T[number].

  • I can turn a frozen list into a union with (typeof list)[number].

  • I can explain why User[key] fails and User[typeof key] works.