Indexed access types
Look up a property's type with Type["key"], every property's type with keyof, and an array's item type with [number], so your types follow your data.
- 01Look up a property's typeWrite
Product["price"]to get the type of one property, and chain lookups into nested objects. - 02Look up many at onceUse a union of keys,
keyof, and[number]to get the types of several properties or of an array's items. - 03Build types from dataRead a type from a value with
typeof, so a list of options and its type never drift apart.
Ask a type for one of its parts
In JavaScript, you read a property of an object with its name in square brackets (the Object basics lesson covers this):
const tea = { name: "Tea", price: 3, inStock: true };
console.log(tea["price"]);Line 1 makes a tea object with a name, a price, and whether it's in stock. Line 2 reads its price with tea["price"] and prints 3.
TypeScript lets you do the same with a type. Write a property name in square brackets after an object type, and you get the type of that property. This is called an indexed access type: the name in the brackets is the index, and looking it up is the access.
type Product = { name: string; price: number; inStock: boolean }; type Price = Product["price"];// ^?const teaPrice: Price = 3;console.log(teaPrice);Line 1 describes a Product: a name that is text, a price that is a number, and an inStock that is true or false (the Object types lesson covers this syntax). Line 3 makes a new type, Price, by looking up price in Product. The query on line 4 shows the answer: type Price = number. Line 5 uses Price like any other type, and line 6 prints 3.
The lookup happens only in the type, before the code runs. Try other names yourself: type inside the brackets on line 3 and watch the answer after the ^? on line 4 change as you type.
type Product = { name: string; price: number; inStock: boolean }; type Looked = Product["price"];// ^?"price"numberNo errorsProduct["price"] looks up the price property, so Looked is number.
"name" gives string and "inStock" gives boolean. A name the type doesn't have, like "colour", gets an error. So does price without quotes, because then it's the name of a type, not a property name. Empty brackets aren't a lookup at all: Product[] is a list of products. The last two tries, a union of names and keyof Product, are coming up in a later section.
Product["price"] goes into the Product type, finds the price property, and gives back its type: number.A shop keeps a spec sheet for each kind of product. It doesn't say what one jar weighs. It says the weight line holds a number in grams, and the color line holds a word. When you need to know what goes on a line, you look it up on the sheet instead of guessing.
- In real life: A spec sheet lists each detail of a product and what kind of thing it is
- In TypeScript: An object type lists each property and its type
- In real life: You ask: what goes on the weight line?
- In TypeScript: You write
Product["price"] - In real life: The sheet answers: a number, in grams
- In TypeScript: TypeScript answers:
number - In real life: When the sheet is updated, the answer you read is the new one
- In TypeScript: When
Productchanges,Product["price"]changes with it
An indexed access type looks up a property's type. Product["price"] means "the type of price in Product". The brackets work on types the way they work on values, and the answer always follows the type it was read from.
Look up one property's type
A lookup can reach into a nested object, one pair of brackets at a time. Here is a user profile with an address inside it:
type User = { id: number; name: string; address: { city: string; zip: string };}; type Address = User["address"];// ^?type City = User["address"]["city"];// ^?Lines 1 to 5 describe a User. Line 4 says its address is an object with a city and a zip. Line 7 looks up address, and the query on line 8 shows the whole address type. Line 9 uses two pairs of brackets: User["address"] gives the address type, then ["city"] looks up city in it. The query on line 10 shows string.
In JavaScript you could also write tea.price with a dot. On a type, the dot is an error, on purpose here:
type Product = { name: string; price: number; inStock: boolean }; type Price = Product.price;Line 3 writes Product.price. TypeScript says Cannot access 'Product.price' because 'Product' is a type, but not a namespace. A namespace is a name that holds other names, which a dot can reach into. A type isn't one, so the dot can't reach into it. The message ends with the fix: Did you mean … with 'Product["price"]'?
The brackets take a type, not a value. So a variable that holds a name doesn't work there either:
type Product = { name: string; price: number; inStock: boolean }; const key = "price";type Price = Product[key];Line 3 stores the text "price" in a variable called key. Line 4 puts key in the brackets, and TypeScript reports two errors on that line:
Type 'key' cannot be used as an index typemeans "I can't look anything up with this".'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?says why:keyexists while the page runs, and types don't.
The message suggests the fix. In a type, typeof followed by a variable name gives that variable's type (the keyof & typeof lesson covers it):
type Product = { name: string; price: number; inStock: boolean }; const key = "price";type Price = Product[typeof key];// ^?Line 3 is the same const key = "price". A const can never change, so TypeScript gives key the exact type "price", a type that allows only that one text (the Literal types lesson covers this). Line 4 uses typeof key, which is that type, so the lookup works. The query on line 5 shows number.
Why look a type up instead of writing number? Because a lookup isn't a copy. It always means "whatever price is now". Here setPrice types its price parameter with Product["price"]. Press the buttons to change the price type on line 1, and watch the query on line 4:
type Product = { name: string; price: number; inStock: boolean }; function setPrice(product: Product, price: Product["price"]) { // ^? return { ...product, price };} const tea: Product = { name: "Tea", price: 3, inStock: true };console.log(setPrice(tea, 4).price);price: numbernumberNo errorsprice is a number. The query on line 4 shows the parameter is number too, so setPrice(tea, 4) is fine and prints 4.
Line 3 never changes, yet the parameter follows line 1 every time. With price: string, lines 8 and 9 get errors, because they still pass numbers: TypeScript has found the two places you must update. With price: number | null, the parameter accepts null too. If line 3 said price: number, you would have to remember to change it by hand.
Look up several properties at once
Inside the brackets you can put more than one name, joined with |. That makes a union of names, "this one or that one" (the Union types lesson covers unions). TypeScript looks up each name and joins the answers:
type Product = { name: string; price: number; inStock: boolean }; type NameOrPrice = Product["name" | "price"];// ^?type AnyValue = Product[keyof Product];// ^?Line 3 looks up "name" | "price". The query on line 4 shows string | number: the type of either one. Line 5 uses keyof Product. As in the keyof & typeof lesson, keyof before an object type gives the union of all its property names, here "name" | "price" | "inStock". So Product[keyof Product] is the type of any one of its properties, and the query on line 6 shows string | number | boolean.
Product["name" | "price"] looks up each name in the union, then joins the two answers into one union: string | number.That "any property" type is useful, but it can be too loose. Here is a function that saves one setting on a settings page. Its value parameter is the type of any setting:
type Settings = { theme: "light" | "dark"; fontSize: number; showTips: boolean }; function updateSetting(key: keyof Settings, value: Settings[keyof Settings]) { console.log(`Saving ${key} = ${value}`);} updateSetting("fontSize", "dark");Line 1 describes the settings. theme is "light" | "dark", which allows only those two texts. Line 3 says key is any setting name and value is Settings[keyof Settings], which is number | boolean | "light" | "dark". Line 4 prints what it saves. Line 7 saves the text "dark" as the font size. There are no errors, and it prints Saving fontSize = dark. The value type doesn't know which key you passed.
The fix is to make the key a type parameter, a name for a type that is filled in at each call (the Generic constraints lesson covers K extends keyof T). Then look up Settings[K]: the type of the one key that was passed. Press the buttons to try different calls on line 7:
type Settings = { theme: "light" | "dark"; fontSize: number; showTips: boolean }; function updateSetting<K extends keyof Settings>(key: K, value: Settings[K]) { console.log(`Saving ${key} = ${value}`);} updateSetting("fontSize", 18);Saving fontSize = 18numberNo errorsK is "fontSize", so Settings[K] is number. 18 is a number: no errors.
Line 3 now says K extends keyof Settings: K must be one of the setting names, and it is the exact name you pass. value: Settings[K] looks that name up. So with "fontSize", the value must be a number, and with "theme", it must be "light" or "dark". The two wrong calls still print when they run, but TypeScript marks them first.
The type of an array's items
An array type like string[] means "a list of strings" (the Arrays & tuples lesson covers them). To get the type of one item, look it up with number in the brackets:
type Cart = { name: string; price: number }[]; type CartItem = Cart[number];// ^?const cake: CartItem = { name: "Cake", price: 4 };console.log(cake.name);Line 1 says a Cart is a list of objects, each with a name and a price. Line 3 looks up Cart[number]. Array items sit at number positions, 0, 1, 2, and so on, so [number] means "the item at any number position". The query on line 4 shows the item type. Line 5 uses it for cake, and line 6 prints Cake.
Cart[0] gives the same type. An array type doesn't know how many items the list will hold, so every position has the same type. [number] is the usual way to write it, because it says "any item".
Cart has the same type, so Cart[number] asks for the type of any one of them: { name: string; price: number }.A tuple is different: it has a fixed length and its own type at each position. So the position you look up matters, and one past the end is an error, on purpose here:
type CartLine = [string, number]; type Name = CartLine[0];// ^?type Either = CartLine[number];// ^?type Third = CartLine[2];Line 1 says a CartLine is a name and then a quantity, like ["Tea", 2]. Line 3 looks up position 0, and the query on line 4 shows string. Line 5 looks up number, any position, and the query on line 6 shows string | number. Line 7 asks for position 2. TypeScript says Tuple type 'CartLine' of length '2' has no element at index '2': the positions are only 0 and 1.
The table below sums up what can go inside the brackets. Read each row as: what you put in, an example, and what you get.
| Inside the brackets | Example | What you get |
|---|---|---|
| A property name in quotes | Product["price"] | That property's type: number. |
Several names joined with | | Product["name" | "price"] | Each one's type, joined: string | number. |
keyof and the type | Product[keyof Product] | The type of any property: string | number | boolean. |
number, after an array type | Cart[number] | The type of any one item in the array. |
| A position, after a tuple type | CartLine[0] | The type at that position: string. |
Types read from your data
So far the types came first. Often the data comes first: the sizes in a dropdown, or a page's default settings. Then you can read the type from the data, so the two can never disagree:
const sizes = ["S", "M", "L"] as const; type Size = (typeof sizes)[number];// ^?function pickSize(size: Size) { console.log(`You picked ${size}`);}pickSize("M");Line 1 is a list of sizes for a dropdown. as const after it tells TypeScript the list will never change, so it keeps the exact texts "S", "M", and "L" instead of "any string" (the as const & satisfies lesson covers it).
Line 3 does two steps. typeof sizes gives the type of the value sizes. Then [number] takes the type of any one item. The round brackets around typeof sizes aren't required, but they make it clear which step comes first. The query on line 4 shows "S" | "M" | "L". Lines 5 to 7 make a function that takes only a Size, and line 8 prints You picked M.
typeof sizes reads its type, and [number] takes the type of any one item: "S" | "M" | "L". Add "XL" to the list and the type follows.The list is now the only place the sizes are written. Change it with the buttons, and watch line 8, which asks for an XL:
const sizes = ["S", "M", "L"] as const; type Size = (typeof sizes)[number];// ^?function pickSize(size: Size) { console.log(`You picked ${size}`);}pickSize("XL");"S" | "M" | "L"An error on line 81 error: line 8There is no "XL" in the list, so there is none in Size. Line 8 gets Argument of type '"XL"' is not assignable to parameter of type '"S" | "M" | "L"'.
Adding "XL" to line 1 is the only change, and the error on line 8 disappears, because Size now includes it. Taking sizes away makes calls to missing sizes errors again. You never edit a type by hand.
The same works for objects. Here a page's default settings give you the Settings type:
const defaultSettings = { theme: "light", fontSize: 16, showTips: true }; type Settings = typeof defaultSettings;// ^?type FontSize = Settings["fontSize"];// ^?Line 1 is an object with the default settings. Line 3 makes Settings its type, and the query on line 4 shows it. Line 5 looks up fontSize in it, and the query on line 6 shows number. Notice that theme is string, not "light": an object's properties can change, so TypeScript keeps the general type unless you add as const.
Types for a todo list from one reply type
A todo app gets one reply from its server: the user and their todos. Describe the reply once, and look up every smaller type from it:
type TodosReply = { user: { id: number; name: string }; todos: { id: number; title: string; done: boolean }[];}; type Todo = TodosReply["todos"][number];type TodoId = Todo["id"]; function findTodo(reply: TodosReply, id: TodoId) { return reply.todos.find((todo) => todo.id === id);} const reply: TodosReply = { user: { id: 7, name: "Asha" }, todos: [ { id: 1, title: "Buy tea", done: false }, { id: 2, title: "Bake cake", done: true }, ],};console.log(findTodo(reply, 2)?.title);Lines 1 to 4 describe what the server sends: a user, and todos, a list of objects. Line 6 makes Todo in two steps: TodosReply["todos"] is the list, and [number] is one item of it. Line 7 makes TodoId the type of a todo's id, which is number. Lines 9 to 11 look up a todo by its id with find (the Searching arrays lesson covers it). Lines 13 to 19 are a sample reply, and line 20 prints Bake cake.
Now say the server starts sending ids as text. You change id: number on line 3, and nothing else. Todo and TodoId follow, and TypeScript marks every place that still uses a number id. That's why teams describe each server reply once and look up the rest.
type P = { price: number }; type A = P["price"];type P = { price: number }; type A = P["cost"];type P = { price: number }; type A = P.price;type Tags = string[]; type Tag = Tags[number];type P = { price: number; name: string }; type A = P["price" | "name"];const key = "price"; type P = { price: number }; type A = P[key];const key = "price"; type P = { price: number }; type A = P[typeof key];type Pair = [string, number]; type A = Pair[2];
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
A type read from data is only as exact as the data's type. Here is the size list again, without as const:
const sizes = ["S", "M", "L"]; type Size = (typeof sizes)[number];// ^?function pickSize(size: Size) { console.log(`You picked ${size}`);}pickSize("XXXL");Line 1 has no as const, so TypeScript types the list as string[]: a list of any texts, because you might add more later. The query on line 4 shows type Size = string. Line 8 asks for a size that doesn't exist, and TypeScript has nothing to say. It prints You picked XXXL.
- "
Product.priceworks on a type, like in JavaScript." No. A type is looked up with brackets:Product["price"]. The dot is an error. - "I can put a variable in the brackets." No. The brackets take a type. For a
const, writetypeof keyto use its type. - "
Product["price"]is a copy of the type." No. It followsProduct. Changeprice, and every lookup of it changes too. - "
Product["name" | "price"]gives an object with those two properties." No. It gives their types, joined:string | number. - "
Cart[number]means the item at a position called number." No. It means any number position, so it gives the type of one item. - "
(typeof sizes)[number]always gives the exact values." No. Only when the list hasas const. Without it, the answer isstring. - "A lookup reads the object while the page runs." No. It's a type, so it's checked and then removed before the code runs, like every type.
The table below sets the brackets on a value beside the brackets on a type. Read each row as: what you write, what it is, and what you get.
| When you write | What it is | What you get |
|---|---|---|
tea["price"] | A JavaScript property read on a value. It runs with the page. | The value: 3. |
Product["price"] | An indexed access type on a type. It's removed before the code runs. | The type: number. |
Product.price | An error. A type is looked up with brackets, not a dot. | Cannot access 'Product.price' because 'Product' is a type, but not a namespace. |
Product[key] with const key = "price" | An error. The brackets take a type, and key is a value. | Write Product[typeof key] to use the value's type. |
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code: the errors and ^? answers update as you type. Try a guess first, then use the hints.
What type is Status? Guess first. Then press Edit & run and add a // ^? line under it to check.
type Order = { id: number; status: "paid" | "sent"; total: number }; type Status = Order["status"];type Order = { id: number; status: "paid" | "sent"; total: number }; type Status = Order["status"];// ^?Order["status"] is the type of status, which is "paid" | "sent". The query on line 4 shows type Status = "paid" | "sent".
Three lookups on a user type, one with a mistake. Which line will TypeScript mark?
type User = { id: number; name: string; email: string };
type UserId = User["id"];
type UserName = User.name;
type UserEmail = User["email"];type User = { id: number; name: string; email: string }; type UserId = User["id"];type UserName = User["name"];type UserEmail = User["email"];Line 4 writes User.name with a dot, and a type can't be read with a dot. User["name"] fixes it, and the type is string.
Line 6 gets an error, because Song is unknown, a type you can't read anything from. Replace unknown on line 3 with the type of one song from Playlist.
type Playlist = { title: string; seconds: number }[]; type Song = unknown; // replace unknown const song: Song = { title: "Rain", seconds: 200 };console.log(song.title);type Playlist = { title: string; seconds: number }[]; type Song = Playlist[number]; const song: Song = { title: "Rain", seconds: 200 };console.log(song.title);Playlist[number] is the type of any one song: { title: string; seconds: number }. Now song.title is a string, the error on line 6 is gone, and the code prints Rain.
The shop only sells in dollars and euros, but formatPrice accepts any text. Type its parameters by looking them up in Product. Which line does TypeScript mark then?
type Product = { name: string; price: number; currency: "USD" | "EUR" }; function formatPrice(price: number, currency: string) { return `${price} ${currency}`;} console.log(formatPrice(3, "INR"));type Product = { name: string; price: number; currency: "USD" | "EUR" }; function formatPrice(price: Product["price"], currency: Product["currency"]) { return `${price} ${currency}`;} console.log(formatPrice(3, "INR"));With currency: Product["currency"], the parameter is "USD" | "EUR". Line 7 passes "INR", so TypeScript marks it: Argument of type '"INR"' is not assignable to parameter of type '"USD" | "EUR"'.
There is no "owner" role, but TypeScript doesn't complain about line 8. What do you add to line 1 so it does?
const roles = ["viewer", "editor", "admin"]; type Role = (typeof roles)[number];// ^?function setRole(role: Role) { console.log(`Role set to ${role}`);}setRole("owner");const roles = ["viewer", "editor", "admin"] as const; type Role = (typeof roles)[number];// ^?function setRole(role: Role) { console.log(`Role set to ${role}`);}setRole("owner");With as const, the list keeps its exact texts, so Role is "viewer" | "editor" | "admin". Line 8 passes "owner", and TypeScript marks it.
Line 7 saves true as the font size, and TypeScript allows it. Change line 3 so the value must match the key. Then: what type must value be when the key is "fontSize"?
type Settings = { theme: "light" | "dark"; fontSize: number; showTips: boolean }; function updateSetting(key: keyof Settings, value: Settings[keyof Settings]) { console.log(`Saving ${key} = ${value}`);} updateSetting("fontSize", true);type Settings = { theme: "light" | "dark"; fontSize: number; showTips: boolean }; function updateSetting<K extends keyof Settings>(key: K, value: Settings[K]) { console.log(`Saving ${key} = ${value}`);} updateSetting("fontSize", true);K is the exact key you pass, so Settings[K] is that setting's type. For "fontSize" it's number, so true on line 7 gets Argument of type 'boolean' is not assignable to parameter of type 'number'.
Check your understanding
For each question, ask: which type is being looked up, and what is inside the brackets? The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
Product["price"]give you, ifProducthasprice: number?Choose an answer to see the explanation.
Question 2 of 7What type is
Pickedin this order lookup?Read the code, then predicttype Order = { id: number; total: number; note: string }; type Picked = Order["id" | "note"];Choose an answer to see the explanation.
Question 3 of 7What type is
Todo, looked up from a list of todos?Read the code, then predicttype Todos = { title: string; done: boolean }[]; type Todo = Todos[number];Choose an answer to see the explanation.
Question 4 of 7This list has no
as const. What type isColor?Read the code, then predictconst colors = ["red", "blue"]; type Color = (typeof colors)[number];Choose an answer to see the explanation.
Question 5 of 7Which line does TypeScript mark in these profile lookups?
Read the code, then predicttype Profile = { name: string; age: number }; type Name = Profile["name"]; type Age = Profile.age;Choose an answer to see the explanation.
Question 6 of 7Why does TypeScript mark line 4 of this profile lookup?
Read the code, then predicttype Profile = { name: string; age: number }; const field = "age"; type Age = Profile[field];Choose an answer to see the explanation.
Question 7 of 7You change
price: numbertoprice: stringinProduct. What happens to a parameter typedProduct["price"]?Choose an answer to see the explanation.
Key takeaways
Type["key"]looks up the type of one property, likeProduct["price"]isnumber. Chain brackets to reach into nested objects.- Types are looked up with brackets, never a dot, and the brackets take a type, so a variable needs
typeof. - A union of keys gives a union of types.
Type[keyof Type]is the type of any property. - With a type parameter,
Settings[K]is the type of the exact key passed, which keeps a key and its value matched. Array[number]is the type of one item. A tuple looks up each position on its own.(typeof list)[number]reads a type from your data. Addas constto keep the exact values.- A lookup isn't a copy: it follows the type it reads from, so one change updates every lookup.
Remember the one-liner.
Brackets on a type ask for the type inside it, and the answer always follows the type you asked.
Coming next: Conditional types. They let a type choose between two answers, like "if this is an array, give its item type".