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 propertyKinT. - 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 giveundefined. - as const
- A suffix that freezes a value into its exact literal types.
- Index type
- The type you put in the brackets, like
"name",numberor 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.
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 | number02Every 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.
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"]; // number04Tuples 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
Predict the union
In the first example write
type Both = User["name" | "address"];and predict the result before hovering. It'sstring | { city: string; pin: string }. - 2
Make an element optional
In the second example change
itemstoitems?: { ... }[]. NowOrder["items"][number]is an error (TS2537: Type '...[] | undefined' has no matching index signature for type 'number') andItemandPricebecomeany: the array might beundefined, and you can't indexundefined. Fix it withNonNullable<Order["items"]>[number](NonNullable is in Topic 10.2). - 3
Frozen or loose
In the second example delete
as constfromcolors. PredictColorfirst (it becomesstring), and what happens to the"pink"line.
Code & diagrams
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)[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
redEach 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)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 8080Break 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.
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.
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];.
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 untilTandKare known, so it reasons with rules instead.K extends keyof Tis exactly what makesT[K]valid. Without the constraint you getType 'K' cannot be used to index type 'T'. - ▸
Indexing with a union distributes:
T["a" | "b"]isT["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
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 thenameproperty. This is an indexed access type. It lets you reuse a property's type without writing it a second time, so ifnamechanges fromstringto a richer type, everything that points at it follows. - 2
You can chain lookups to go deep:
User["address"]["city"]first finds the type ofaddress, then the type ofcityinside 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
The index can be a union of key names, and the result is the union of the property types:
User["id" | "name"]isnumber | string. Withkeyof(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"]includesundefined, because reading it could giveundefined. - 4
The most useful trick is
T[number]. An array type likeItem[]can be read with anynumberindex, andItem[][number]is the type you get back:Item. SoOrder["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
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 number2. Asking for a slot that doesn't exist, likePair[2], is an error.Pair[number]is the union of all slot types. - 6
Combine with
typeofandas constto 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
Two rules to remember. First, the index must be a type, not a value:
User[key]wherekeyis a variable is an error, butUser[typeof key]is fine. Second, in genericsT[K]is the heart of safe property access (Topic 7.3); the compiler tracks that the result depends on whichKyou chose. Finally,noUncheckedIndexedAccessadds| undefinedto *expressions* likearr[0], but not to the typeArr[0], which stays the plain element type.
Explain it without notes
What does T["key"] do, and how is it different from obj["key"]?
How do you get the type of one element of an array that is a property of another type?
What type does User["a" | "b"] produce, and what about User[keyof User]?
Why is User[key] an error when key is a const variable?
Practice
Given type Api = { users: { id: number; email: string }[]; total: number }, write type UserRow (the type of one user) and type Email.
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 withtypeonce 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 buildingOrderfrom 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 andUser[typeof key]works.