Arrays & tuples
Type a list of values with number[] or Array<number>, a fixed shape like [string, number] with a tuple, and protect lists from changes with readonly arrays.
- 01Type a listWrite an array type like
number[], read the one TypeScript infers, and know what it can't check about length. - 02Use a tupleDescribe a fixed shape like a name and a price with
[string, number], including optional and rest elements. - 03Protect an arrayMark an array
readonlyso a function can't push, sort, or change its items, and use methods that make copies.
A list where every item is the same kind
A shop page is full of lists: the prices in a cart, the tags on a product, the searches someone made. In JavaScript, a list is an array (Arrays covers them). TypeScript gives every array a type that says what kind of item it holds. Here are three prices, with no type written:
const prices = [3, 4, 5];// ^?prices.push(6);console.log(prices);Line 1 makes an array of three numbers. The query on line 2 shows the type TypeScript inferred: const prices: number[]. Read number[] as "an array of numbers". The square brackets after a type mean "a list of these". Line 3 adds a fourth price with push, and line 4 prints [3, 4, 5, 6].
The type says what goes in the array, not how many items there are. So adding a number is fine, but adding anything else isn't:
const prices: number[] = [3, 4, 5];prices.push(6);prices.push("free");console.log(prices.length);Line 1 writes the type itself, prices: number[]. Line 2 pushes 6, a number. Line 3 pushes the text "free", and TypeScript marks it on purpose: Argument of type 'string' is not assignable to parameter of type 'number'. An argument is a value you pass to a function, here to push. The message means "push on this array only takes numbers". When it runs anyway, the text goes in, and line 4 prints 5.
prices is a number[]: any number of numbers, like the 6 that line 2 adds. The text "free" on line 3 is a string, so TypeScript won't let push put it in.Some shops stack their coins in tubes. Each tube is made for one size of coin. You can drop in as many of that coin as you like, and you know what's inside without tipping it out. A coin of a different size won't go in.
- In real life: A tube made for one size of coin
- In TypeScript: An array type like
number[] - In real life: Add as many of those coins as you like
- In TypeScript:
pushas many numbers as you like - In real life: A coin of another size won't go in
- In TypeScript:
push("free")gets an error - In real life: The label tells you what's inside without counting
- In TypeScript: The type tells you every item is a number, not how many there are
An array type says what kind every item is. number[] is an array of numbers, of any length. A tuple type, like [string, number], goes further: it says how many items there are and what each position holds. Either can be readonly, so it can be read but not changed.
Writing an array type
There are two ways to write an array type. They mean exactly the same thing:
const tags: string[] = ["tea", "green"];const prices: Array<number> = [3, 4, 5];// ^?const flags: boolean[] = [true, false];console.log(tags.length, prices.length, flags.length);Line 1 writes string[]: an array of strings, for a product's tags. Line 2 writes Array<number>. The angle brackets hold the item type, so read it as "an Array of number". The query on line 3 shows TypeScript treats it as number[], the same type. (Angle brackets like these are called generics, and get lessons of their own later.) Line 4 is an array of booleans. Line 5 prints 2 3 2, the three lengths.
Most code uses the short form, number[]. Pick one and use it everywhere in a project.
Like any value, an array usually doesn't need a type written: TypeScript infers it from the items. Press each button to start the cart with different items, and watch the query on line 2:
const cart = [3, 4, 5];// ^?cart.push(6);console.log(cart);number[]No errors[3, 4, 5, 6]Every item is a number, so TypeScript infers number[], an array of numbers. Line 3 adds the number 6, which fits.
Numbers give number[], and text gives string[], which makes line 3's push(6) an error. Mixed items give a type like (string | number)[]. The | means "or": every item is a string or a number (the Union types lesson covers |). The round brackets matter: (string | number)[] is one array of mixed items, but string | number[] means "a string, or an array of numbers".
There is one array TypeScript can't read a type from: an empty one. Here is a function that starts an empty list of recent searches:
function recentSearches() { const searches = []; return searches;}Line 2 makes an empty array, and line 3 returns it. There are no items to infer from, and nothing is added before line 3 hands it back, so TypeScript reports, on purpose here: Variable 'searches' implicitly has type 'any[]' in some locations where its type cannot be determined. In plain words: "I can't tell what this array will hold, so I'd have to stop checking it". Line 3 gets a matching error. (If the function pushed strings in before returning, TypeScript would follow the pushes and settle on string[]. Writing the type is still clearer.) The fix is the habit from the last lesson: annotate what TypeScript can't see.
function recentSearches() { const searches: string[] = []; return searches;}const recent = recentSearches();recent.push("green tea");console.log(recent);Line 2 now says searches is a string[]. The errors are gone. Line 5 calls the function, line 6 pushes a search into the list it gave back, and line 7 prints ["green tea"].
Because TypeScript knows the item type, array methods know it too. The functions you pass to map and find get their parameter types from the array, with no annotation (map, filter, and reduce covers these methods):
const prices = [3, 4, 5];const doubled = prices.map((price) => price * 2);// ^?const expensive = prices.find((price) => price > 4);// ^?console.log(doubled, expensive);On line 2, price is a number because prices is a number[], so price * 2 is number math. The query on line 3 shows map gives back number[]. Line 4 uses find, and the query on line 5 shows number | undefined: a number, or undefined when nothing matches. Line 6 prints [6, 8, 10] 5.
An array type has one gap: it doesn't know how many items there are. Reading past the end gets no error:
const prices = [3, 4, 5];const fourth = prices[3];// ^?console.log(fourth);Line 2 reads prices[3], the fourth item. There are only three. The query on line 3 still says const fourth: number, and there is no error, but line 4 prints undefined. TypeScript assumes every index you read is there. A compiler option called noUncheckedIndexedAccess changes that. This example turns it on; the list under the code shows the option:
const prices = [3, 4, 5];const fourth = prices[3];// ^?console.log(fourth.toFixed(2));Now the query on line 3 says number | undefined, and line 4 gets 'fourth' is possibly 'undefined', on purpose. That's true: when it runs, line 4 throws a TypeError. The option isn't part of strict, so many projects leave it off, and every other example in these lessons does too.
Tuples for fixed shapes
Sometimes a list has a fixed shape: a product name and a quantity, always in that order. A tuple type describes that. You write the type of each position inside square brackets:
const teaLine: [string, number] = ["Tea", 3];const [name, quantity] = teaLine;// ^?console.log(name.toUpperCase(), quantity * 2);Line 1 says teaLine holds exactly two items: item 0 is a string and item 1 is a number. Line 2 takes it apart with destructuring, and the query on line 3 shows const name: string. So on line 4, name.toUpperCase() and quantity * 2 are both allowed, and it prints TEA 6.
string[] says what every item is, for any number of items. A tuple type like [string, number] says what each position holds: exactly two items, a string and then a number.From square brackets alone, TypeScript infers an array, not a tuple. (The Literal types lesson shows as const, which changes that.) Try line 1 without a type, with an array type, with the tuple type, and with the tuple in the wrong order:
const teaLine = ["Tea", 3];const [name, quantity] = teaLine;// ^?console.log(name.toUpperCase(), quantity * 2);string | numberno type2 errors: line 4With no type, ["Tea", 3] is inferred as (string | number)[], so name might be a number. Line 4 gets Property 'toUpperCase' does not exist on type 'string | number', and quantity * 2 gets The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type: TypeScript can't be sure quantity is a number.
With no type, or with (string | number)[], TypeScript only knows each item is a string or a number, so both uses on line 4 get errors. With [string, number], it knows which is which, and the errors go away. With [number, string], line 1 gets errors because the values are the wrong way round. A tuple's order is part of its type.
A school lunch tray has a compartment for each part of the meal: a big one for rice, a round one for soup. Everyone knows where each thing goes, and a tray with soup where the rice should be looks wrong straight away. There's no extra compartment, so nothing else fits.
- In real life: A tray with two compartments
- In TypeScript: A tuple type with two positions:
[string, number] - In real life: The left one is for rice, the right one for soup
- In TypeScript: Item 0 is a
string, item 1 is anumber - In real life: Soup in the rice compartment is noticed at once
- In TypeScript:
[3, "Tea"]gets an error - In real life: There's no third compartment to fill
- In TypeScript:
["Cake", 4, 2]gets an error
A tuple also knows its length, so it catches the gap an array type has:
const teaLine: [string, number] = ["Tea", 3];const cakeLine: [string, number] = ["Cake", 4, 2];console.log(teaLine[2]);Both errors are on purpose:
- Line 2 gives
cakeLinethree items. The message ends withSource has 3 element(s) but target allows only 2: "you gave three, the type takes two". An element is an item in an array. - Line 3 reads index 2:
Tuple type '[string, number]' of length '2' has no element at index '2'. When it runs, it printsundefined.
You'll meet tuples in code you didn't write. Object.entries turns an object into a list of key-and-value pairs, and each pair is a tuple:
const stock = { tea: 3, cake: 0 };const rows = Object.entries(stock);// ^?for (const [name, count] of rows) { console.log(name, count);}Line 1 is the stock of two products. Line 2 calls Object.entries, and the query on line 3 shows [string, number][]: an array of tuples, each a name and a count. Line 4 takes each pair apart in a for…of loop, so name is a string and count is a number. It prints tea 3, then cake 0.
The table below compares the two kinds of list. Read each row as a question, then the answer for each.
| Question | An array type | A tuple type |
|---|---|---|
| How many items can it hold? | Any number, including none. | A fixed number: exactly two for [string, number]. |
| What does the type say about each item? | Every item has the same type, like string. | Each position has its own type: item 0 is a string, item 1 is a number. |
| What does reading past the end give? | The item type, like string, with no error, even though the value is undefined. | An error: Tuple type '[string, number]' of length '2' has no element at index '2'. |
| Does TypeScript infer it from square brackets? | Yes. ["tea", "green"] gets string[]. | No. You write the tuple type yourself, or get one from a function like Object.entries. |
Optional and rest elements
Some fixed shapes have a part that may be missing. A color for a page theme is three numbers, red, green, and blue, and sometimes a fourth for how see-through it is. A ? after a tuple element makes it optional:
type Color = [number, number, number, number?]; const orange: Color = [255, 165, 0];const glass: Color = [255, 165, 0, 0.5];const [red, green, blue, alpha] = glass;// ^?console.log(orange.length, glass.length);// ^?Line 1 gives the shape a name with a type alias, type Color = …, so it can be reused (the Type aliases & interfaces lesson covers them). The number? at the end is the optional element. Line 3 gives orange three numbers, and line 4 gives a see-through glass color four. Both fit. Line 5 takes glass apart, and the query on line 6 shows const alpha: number | undefined: the fourth item may not be there. Line 7 prints 3 4, and the query on line 8 shows length: 3 | 4. TypeScript knows the length is 3 or 4. Optional elements go at the end: a required element can't follow one, so [number?, number] is an error.
Other shapes have a fixed start and then any number of items. A rest element, written ... before an array type, means "any number of these" (like rest syntax in JavaScript). A page's breadcrumb, the trail of links at the top like Home / Shop / Tea, always has at least one page:
type Breadcrumb = [string, ...string[]]; const trail: Breadcrumb = ["Home", "Shop", "Tea"];const home: Breadcrumb = ["Home"];const nowhere: Breadcrumb = [];console.log(trail.join(" / "), home.length);Line 1 says a Breadcrumb starts with one string, followed by any number of strings: ...string[]. Lines 3 and 4 fit, with three pages and with one. Line 5 tries an empty trail, and TypeScript marks it on purpose: Source has 0 element(s) but target requires 1. A plain string[] would allow [], and code that reads trail[0] would get undefined. Line 6 prints Home / Shop / Tea 1.
The table below shows how many items each type allows. Read each row as: this type, how many items it takes, and a value that fits.
| The type | How many items it takes | A value that fits |
|---|---|---|
string[] | Any number, including none. | [] or ["Home", "Shop"] |
[string, number] | Exactly two. | ["Tea", 3] |
[number, number, number, number?] | Three or four. | [255, 165, 0] or [255, 165, 0, 0.5] |
[string, ...string[]] | One or more. | ["Home"] or ["Home", "Shop", "Tea"] |
Read-only arrays
Some arrays shouldn't change after they're made, like a product's tags shown on its page. Put readonly before the type, and TypeScript allows reading but not changing:
const tags: readonly string[] = ["tea", "green"];tags.push("organic");tags[0] = "coffee";const loud = tags.map((tag) => tag.toUpperCase());console.log(loud);Line 1 says tags is a readonly string[]. Two lines get errors, on purpose:
- Line 2:
Property 'push' does not exist on type 'readonly string[]'. A read-only array type has no methods that change it: nopush,pop,sort, orsplice. - Line 3:
Index signature in type 'readonly string[]' only permits reading. "Index signature" means reading or writing by position, liketags[0]. You may read it, not store a new value.
Line 4 is fine: map makes a new array and leaves tags alone. Run it anyway, and line 5 prints ["COFFEE", "GREEN", "ORGANIC"]: lines 2 and 3 really did change the array. readonly is a check TypeScript makes, and like every type it's removed before the code runs. You can also write ReadonlyArray<string>; it's the same type.
const and readonly guard different things. const stops the name from getting a different array. readonly stops the array from being changed. The table below compares them. Read each row as: what you try, then what happens with each declaration.
| What you try | With const tags: string[] | With const tags: readonly string[] |
|---|---|---|
tags.push("organic") | Allowed. | An error: Property 'push' does not exist on type 'readonly string[]'. |
tags[0] = "coffee" | Allowed. | An error: Index signature in type 'readonly string[]' only permits reading. |
tags = ["cake"] | An error: Cannot assign to 'tags' because it is a constant. | The same error. It comes from const, not from readonly. |
tags.map(…) or tags.toSorted() | Allowed. | Allowed. These make a new array and leave this one alone. |
Where readonly helps most is a function's parameter. Passing an array to a function doesn't copy it, so a function that sorts its parameter sorts your array. Switch the parameter type and the method, and watch the cart in the output:
function cheapestFirst(prices: number[]) { return prices.sort((a, b) => a - b);} const cart = [5, 3, 4];const sorted = cheapestFirst(cart);console.log(sorted, cart);No errors[3, 4, 5][3, 4, 5]No errors, but look at the output: cart is sorted too. sort reorders the array it's called on, and prices and cart are the same array. The shopper's cart order is lost.
With number[] and sort, there are no errors, but cart comes out sorted: the shopper's order is lost. With readonly number[], TypeScript stops sort on line 2. With toSorted, which sorts a copy (Sorting arrays covers both), the error is gone and cart keeps its order. A readonly parameter is a promise: "this function won't change what you pass in".
cheapestFirst(cart) doesn't copy the array: prices and cart are two names for one array. sort reorders that one array, so the cart changes too. readonly number[] stops the function from calling sort.Recent searches in a search box
A search box remembers your last three searches, newest first, with no repeats. Here is the function that adds one, with a readonly list coming in and a new list going out:
function addSearch(recent: readonly string[], term: string): string[] { const others = recent.filter((item) => item !== term); return [term, ...others].slice(0, 3);} let recent: readonly string[] = ["tea", "cake"];recent = addSearch(recent, "scones");recent = addSearch(recent, "tea");console.log(recent);Line by line:
- Line 1:
addSearchtakes the current searches as areadonly string[], so it can't change them, and promises to give back astring[]. - Line 2 uses
filterto make a new array without the new term, so a repeat moves to the front instead of appearing twice.itemis a string, inferred from the array. - Line 3 builds a new array with the term first, then the others, using spread (
...), and keeps the first three withslice. - Line 6 starts the list. It's a
let, so the name can take each new list, but each list is read-only. Lines 7 and 8 addscones, thenteaagain. - Line 9 prints
["tea", "scones", "cake"]: tea moved to the front, and nothing was changed in place.
const prices: number[] = [3, "4"];const prices: Array<number> = [3, 4];const line: [string, number] = ["Tea", 3];const line: [string, number] = [3, "Tea"];const tags: readonly string[] = ["tea"]; tags.push("green");const tags: readonly string[] = ["tea"]; const loud = tags.map((tag) => tag.toUpperCase());const trail: [string, ...string[]] = [];const color: [number, number, number, number?] = [255, 165, 0];
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
- "A
constarray can't change." No.constonly stops the name from getting a different array.pushandsortstill work. Usereadonlyto stop changes to the items. - "TypeScript knows how many items my array has." No.
prices[3]is anumberto TypeScript even when there are only three prices. Tuples know their length; arrays don't. - "
["Tea", 3]is a tuple." No. Without a type, it's inferred as(string | number)[]. Write[string, number]to get a tuple. - "
number[]andArray<number>are different types." No. They're two spellings of the same type. - "
readonlyfreezes the array while the page runs." No. It's a check TypeScript makes, and it's removed with the other types. Another name for the same array, withoutreadonly, can still change it. - "An optional tuple element can go anywhere." No. Optional elements go at the end; a required element can't follow one:
[number?, number]getsA required element cannot follow an optional element.
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code to try your answer: the errors update as you type, the type under the cursor shows below the editor, and the console shows what it prints. Guess first, then use the hints.
No type is written here. What type does TypeScript give ratings? Guess, then check.
const ratings = [4, 5, 3];console.log(ratings.length);const ratings = [4, 5, 3];// ^?console.log(ratings.length);All three items are numbers, so TypeScript infers number[], an array of numbers. The query on line 2 shows const ratings: number[]. It's not a tuple: from square brackets alone, TypeScript infers an array.
Product tags, one with a mistake. Which line will TypeScript mark?
const tags: string[] = ["tea", "green"];
tags.push("organic");
tags.push(42);
console.log(tags.length);const tags: string[] = ["tea", "green"];tags.push("organic");tags.push("loose leaf");console.log(tags.length);tags is a string[], so push only takes strings. Line 3 pushes the number 42: Argument of type 'number' is not assignable to parameter of type 'string'. Pushing a string fixes it.
TypeScript marks line 3 twice. Fix it by adding one type to line 1. What does the fixed code print?
const cakeLine = ["Cake", 4];const [name, price] = cakeLine;console.log(name.toUpperCase(), price + 1);const cakeLine: [string, number] = ["Cake", 4];const [name, price] = cakeLine;console.log(name.toUpperCase(), price + 1);Without a type, cakeLine is a (string | number)[], so name and price could each be either. [string, number] says item 0 is a string and item 1 is a number, so both uses on line 3 are allowed. It prints CAKE 5.
An empty cart, and two errors. What type should items have on line 2?
function emptyCart() { const items = []; return items;}const cart = emptyCart();cart.push("tea");console.log(cart);function emptyCart() { const items: string[] = []; return items;}const cart = emptyCart();cart.push("tea");console.log(cart);items: string[] tells TypeScript what the empty cart will hold, so the implicitly has type 'any[]' errors are gone, and line 6 is checked: only strings can go in.
TypeScript stops line 2, because the parameter is read-only. Fix the function without removing readonly. What does the fixed code print?
function cheapest(prices: readonly number[]) { prices.sort((a, b) => a - b); return prices[0];}const cart = [5, 3, 4];console.log(cheapest(cart), cart);function cheapest(prices: readonly number[]) { const sorted = prices.toSorted((a, b) => a - b); return sorted[0];}const cart = [5, 3, 4];console.log(cheapest(cart), cart);toSorted makes a sorted copy, so it's allowed on a readonly number[] and cart keeps its order. The cheapest is 3, and the cart is still [5, 3, 4]. The starter would have printed 3 [3, 4, 5], with the cart reordered.
TypeScript is happy with this code, but it throws a TypeError when it runs. Change the Breadcrumb type on line 1 so TypeScript catches the empty trail. Which type does it?
type Breadcrumb = string[]; const trail: Breadcrumb = [];console.log(trail[0].toUpperCase());type Breadcrumb = [string, ...string[]]; const trail: Breadcrumb = ["Home"];console.log(trail[0].toUpperCase());With [string, ...string[]], the empty trail on line 3 is an error: Source has 0 element(s) but target requires 1. Giving it one page fixes it, and it prints HOME. The starter has no errors, yet it throws when it runs, because trail[0] is undefined.
Check your understanding
For each question, ask: does the type say what every item is, or what each position is, and can the list be changed? The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What type does TypeScript infer for these ratings?
Read the code, then predictconst ratings = [4, 5, 3];Choose an answer to see the explanation.
Question 2 of 7Without a type, what does TypeScript infer for this name-and-quantity pair?
Read the code, then predictconst teaLine = ["Tea", 3];Choose an answer to see the explanation.
Question 3 of 7Which line does TypeScript flag in this tuple code?
Read the code, then predictconst cakeLine: [string, number] = ["Cake", 4]; console.log(cakeLine[0]); console.log(cakeLine[2]);Choose an answer to see the explanation.
Question 4 of 7What happens when this code reads past the end of an array?
Read the code, then predictconst prices = [3, 4, 5]; const fourth = prices[3]; console.log(fourth);Choose an answer to see the explanation.
Question 5 of 7Which call is allowed on tags: readonly string[]?
Choose an answer to see the explanation.
Question 6 of 7How many items can a [string, ...string[]] hold?
Choose an answer to see the explanation.
Question 7 of 7What does readonly add in const tags: readonly string[]?
Choose an answer to see the explanation.
Key takeaways
number[]andArray<number>both mean an array of numbers. TypeScript infers the type from the items; annotate an empty array.- An array type says what every item is, not how many there are, so reading past the end isn't an error.
- A tuple type like
[string, number]fixes the length and the type at each position. You write it yourself: square brackets alone give an array. ?makes a tuple's last elements optional, and...string[]adds any number of items, as in[string, ...string[]].readonly string[]can be read but not changed. Use it for parameters, and use copying methods liketoSortedandmap.
Remember the one-liner.
An array type says what every item is; a tuple type says what each position is; readonly says hands off.
Coming next: Object types. It shows how to describe an object's shape: its properties, the ones that can be left out, and the ones that can't change.