cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Look up a property's typeWrite Product["price"] to get the type of one property, and chain lookups into nested objects.
  • 02
    Look up many at onceUse a union of keys, keyof, and [number] to get the types of several properties or of an array's items.
  • 03
    Build 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):

Read a property from a valuePop out in the code editor (opens in a new tab)JavaScript
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.

Look up the type of pricePop out in the code editor (opens in a new tab)TypeScript
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.

Playground: look up a property's type
Look up one of Product's typesPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number; inStock: boolean }; type Looked = Product["price"];//   ^?
Inside the brackets"price"
What TypeScript says
Lookednumber
ErrorsNo errors
Try it yourself

Product["price"] looks up the price property, so Looked is number.

The code is checked by the real TypeScript compiler each time you type. Only line 3 changes. Reset puts "price" back.

"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.

A crate labelled Product holds three cards: name: string, price: number, and inStock: boolean. An arrow marked ["price"] leads from the price card to a card that says number.name: stringprice: numberinStock: booleanProductan object type["price"]numberthe property's type
Product["price"] goes into the Product type, finds the price property, and gives back its type: number.
Real-life analogyA product's spec sheet

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 Product changes, Product["price"] changes with it
The main idea

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:

Look into a nested objectPop out in the code editor (opens in a new tab)TypeScript
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:

A dot instead of brackets (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

A variable inside the brackets (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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 type means "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: key exists 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):

The type of a variable, used as the keyPop out in the code editor (opens in a new tab)TypeScript
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:

Playground: change the price type
A parameter typed from a propertyPop out in the code editor (opens in a new tab)TypeScript
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);
Line 1 saysprice: number
What happens
Parameter pricenumber
TypeScriptNo errors
Try it yourself

price is a number. The query on line 4 shows the parameter is number too, so setPrice(tea, 4) is fine and prints 4.

TypeScript checks the code for real each time. Each button changes only line 1. Reset goes back to price: number.

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:

Look up two properties, then all of themPop out in the code editor (opens in a new tab)TypeScript
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.

Two key cards, "name" and "price", each point at their card in the Product crate. An arrow leads out of the crate to a card that says string | number."name""price"the keys you ask forname: stringprice: numberinStock: booleanProductstring | numbertheir types, joined
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:

A settings updater that accepts any setting's valuePop out in the code editor (opens in a new tab)TypeScript
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:

Playground: save one setting
A settings updater with an exact value typePop out in the code editor (opens in a new tab)TypeScript
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);
Console outputSaving fontSize = 18
What happens
Settings[K]number
TypeScriptNo errors
Try it yourself

K is "fontSize", so Settings[K] is number. 18 is a number: no errors.

TypeScript checks the code for real, and the output is what the code prints. Each button changes only line 7. Reset goes back to the first call.

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:

The type of one item in a cartPop out in the code editor (opens in a new tab)TypeScript
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".

A crate labelled Cart holds three cards, each saying { name, price }. An arrow marked [number], any one item, leads to a window called CartItem that lists name: string and price: number.{ name, price }{ name, price }{ name, price }Cartan array type[number]any one itemCartItemname: stringprice: numberthe item type
Every item in 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:

Look up positions in a tuple (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

What can go inside the brackets
Inside the bracketsExampleWhat you get
A property name in quotesProduct["price"]That property's type: number.
Several names joined with |Product["name" | "price"]Each one's type, joined: string | number.
keyof and the typeProduct[keyof Product]The type of any property: string | number | boolean.
number, after an array typeCart[number]The type of any one item in the array.
A position, after a tuple typeCartLine[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:

A size type read from a list of sizesPop out in the code editor (opens in a new tab)TypeScript
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.

A crate labelled sizes holds three gifts: S, M, and L. An arrow marked typeof sizes and [number] leads to a card that says "S" | "M" | "L".SMLsizesthe data, a valuetypeof sizes[number]"S" | "M" | "L"the type Size
The list is data that exists when the page runs. 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:

Playground: change the list of sizes
A size type read from a listPop out in the code editor (opens in a new tab)TypeScript
const sizes = ["S", "M", "L"] as const; type Size = (typeof sizes)[number];//   ^?function pickSize(size: Size) {  console.log(`You picked ${size}`);}pickSize("XL");
The type Size"S" | "M" | "L"
What happens
pickSize("XL")An error on line 8
TypeScript1 error: line 8
Try it yourself

There 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"'.

TypeScript checks the code for real each time. Each button changes only the list on line 1. Reset goes back to 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:

A settings type read from the default settingsPop out in the code editor (opens in a new tab)TypeScript
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:

A todo type looked up from the reply typePop out in the code editor (opens in a new tab)TypeScript
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.

Error or fine?
  • 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];
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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:

A size list without as constPop out in the code editor (opens in a new tab)TypeScript
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.price works 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, write typeof key to use its type.
  • "Product["price"] is a copy of the type." No. It follows Product. Change price, 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 has as const. Without it, the answer is string.
  • "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.

Brackets on a value and on a type
When you writeWhat it isWhat 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.priceAn 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.

Exercise 1 · Warm-upRead a looked-up type

What type is Status? Guess first. Then press Edit & run and add a // ^? line under it to check.

Starter codePop out in the code editor (opens in a new tab)TypeScript
type Order = { id: number; status: "paid" | "sent"; total: number }; type Status = Order["status"];

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the line TypeScript marks

    Three lookups on a user type, one with a mistake. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    type User = { id: number; name: string; email: string };
    
    type UserId = User["id"];
    type UserName = User.name;
    type UserEmail = User["email"];

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeGet the type of one song

      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.

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      type Playlist = { title: string; seconds: number }[]; type Song = unknown; // replace unknown const song: Song = { title: "Rain", seconds: 200 };console.log(song.title);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeLet a type catch a wrong currency

        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?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type Product = { name: string; price: number; currency: "USD" | "EUR" }; function formatPrice(price: number, currency: string) {  return `${price} ${currency}`;} console.log(formatPrice(3, "INR"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeMake a role list catch a typo

          There is no "owner" role, but TypeScript doesn't complain about line 8. What do you add to line 1 so it does?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          const roles = ["viewer", "editor", "admin"]; type Role = (typeof roles)[number];//   ^?function setRole(role: Role) {  console.log(`Role set to ${role}`);}setRole("owner");

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeMake a settings updater exact

            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"?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            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);

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              The indexed access types quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does Product["price"] give you, if Product has price: number?

                Choose an answer to see the explanation.

              2. Question 2 of 7What type is Picked in this order lookup?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Order = { id: number; total: number; note: string };
                
                type Picked = Order["id" | "note"];

                Choose an answer to see the explanation.

              3. Question 3 of 7What type is Todo, looked up from a list of todos?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Todos = { title: string; done: boolean }[];
                
                type Todo = Todos[number];

                Choose an answer to see the explanation.

              4. Question 4 of 7This list has no as const. What type is Color?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const colors = ["red", "blue"];
                
                type Color = (typeof colors)[number];

                Choose an answer to see the explanation.

              5. Question 5 of 7Which line does TypeScript mark in these profile lookups?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Profile = { name: string; age: number };
                
                type Name = Profile["name"];
                type Age = Profile.age;

                Choose an answer to see the explanation.

              6. Question 6 of 7Why does TypeScript mark line 4 of this profile lookup?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Profile = { name: string; age: number };
                
                const field = "age";
                type Age = Profile[field];

                Choose an answer to see the explanation.

              7. Question 7 of 7You change price: number to price: string in Product. What happens to a parameter typed Product["price"]?

                Choose an answer to see the explanation.

              Key takeaways

              • Type["key"] looks up the type of one property, like Product["price"] is number. 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. Add as const to 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".

              CompleteFrontend Clear concepts. Working examples.