cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Type a listWrite an array type like number[], read the one TypeScript infers, and know what it can't check about length.
  • 02
    Use a tupleDescribe a fixed shape like a name and a price with [string, number], including optional and rest elements.
  • 03
    Protect an arrayMark an array readonly so 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:

Three prices in an arrayPop out in the code editor (opens in a new tab)TypeScript
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:

Text pushed into a number array (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

A crate named prices, labelled number[], holds four gifts: 3, 4, 5, and 6. Outside it, a card with the text "free" and an arrow toward the crate, marked: push("free") is an error.prices: number[]3456pricesany number of numbers"free"push("free")is an error
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.
Real-life analogyA coin tube for one size of coin

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: push as 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
The main idea

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:

Two spellings of an array typePop out in the code editor (opens in a new tab)TypeScript
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:

Playground: change what the cart starts with
A cart with no type writtenPop out in the code editor (opens in a new tab)TypeScript
const cart = [3, 4, 5];//    ^?cart.push(6);console.log(cart);
Inferred typenumber[]
What happens
TypeScriptNo errors
Console output[3, 4, 5, 6]
Try it yourself

Every item is a number, so TypeScript infers number[], an array of numbers. Line 3 adds the number 6, which fits.

TypeScript checks the code for real, and the query on line 2 shows the array type it inferred. Each button changes only the items on line 1. Reset goes back to 3, 4, 5.

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:

An empty array with no type (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

The empty array, annotatedPop out in the code editor (opens in a new tab)TypeScript
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):

Array methods know the item typePop out in the code editor (opens in a new tab)TypeScript
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:

Reading past the end of an arrayPop out in the code editor (opens in a new tab)TypeScript
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:

The same read with noUncheckedIndexedAccess (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

A tea line as a tuplePop out in the code editor (opens in a new tab)TypeScript
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.

Left: a crate named tags, labelled string[], holds two gifts, tea and green, and can take any number of strings. Right: a crate named teaLine, labelled [string, number], has two cards, "Tea" and 3. Under them: item 0 is a string, item 1 is a number, exactly two in this order.tags: string[]teagreentagsany number of stringsteaLine: [string, number]"Tea"3teaLine0: string1: numberexactly two, in this order
An array type like 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:

Playground: an array or a tuple
A tea line taken apartPop out in the code editor (opens in a new tab)TypeScript
const teaLine = ["Tea", 3];const [name, quantity] = teaLine;//     ^?console.log(name.toUpperCase(), quantity * 2);
Type of namestring | number
What happens
Line 1 saysno type
TypeScript2 errors: line 4
Try it yourself

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

TypeScript checks the code for real each time. Only the type on line 1 changes. When it runs, every version prints TEA 6, because the types are removed first. Reset goes back to No type.

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.

Real-life analogyA lunch tray with shaped compartments

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 a number
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:

Too many items, and an index that isn't there (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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 cakeLine three items. The message ends with Source 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 prints undefined.

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:

Stock counts as key-and-value tuplesPop out in the code editor (opens in a new tab)TypeScript
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.

Array types and tuple types, side by side
QuestionAn array typeA 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:

A color with an optional fourth numberPop out in the code editor (opens in a new tab)TypeScript
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:

A breadcrumb with at least one page (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

How many items each type takes
The typeHow many items it takesA 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:

Changing a read-only array (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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: no push, pop, sort, or splice.
  • Line 3: Index signature in type 'readonly string[]' only permits reading. "Index signature" means reading or writing by position, like tags[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.

const alone, and const with readonly
What you tryWith 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:

Playground: sort a copy, not the cart
Prices sorted cheapest firstPop out in the code editor (opens in a new tab)TypeScript
function cheapestFirst(prices: number[]) {  return prices.sort((a, b) => a - b);} const cart = [5, 3, 4];const sorted = cheapestFirst(cart);console.log(sorted, cart);
TypeScriptNo errors
Console output
sorted[3, 4, 5]
cart[3, 4, 5]
Try it yourself

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.

TypeScript checks the code for real, and the output is what this code prints. Only lines 1 and 2 change. Reset goes back to number[] and sort.

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

Two tags, cart and prices, each with an arrow to the same crate. The crate holds 3, 4, and 5 after sort. A note says: one array, two names.cartprices345one arrayafter sort, both names see 3, 4, 5
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:

Keeping the last three searchesPop out in the code editor (opens in a new tab)TypeScript
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: addSearch takes the current searches as a readonly string[], so it can't change them, and promises to give back a string[].
  • Line 2 uses filter to make a new array without the new term, so a repeat moves to the front instead of appearing twice. item is 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 with slice.
  • 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 add scones, then tea again.
  • Line 9 prints ["tea", "scones", "cake"]: tea moved to the front, and nothing was changed in place.
Error or fine?
  • 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];
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 const array can't change." No. const only stops the name from getting a different array. push and sort still work. Use readonly to stop changes to the items.
  • "TypeScript knows how many items my array has." No. prices[3] is a number to 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[] and Array<number> are different types." No. They're two spellings of the same type.
  • "readonly freezes 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, without readonly, 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] gets A 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.

Exercise 1 · Warm-upRead an inferred array type

No type is written here. What type does TypeScript give ratings? Guess, then check.

Starter codePop out in the code editor (opens in a new tab)TypeScript
const ratings = [4, 5, 3];console.log(ratings.length);

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

    Exercise 2 · Warm-upFind the line TypeScript marks

    Product tags, one with a mistake. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    const tags: string[] = ["tea", "green"];
    tags.push("organic");
    tags.push(42);
    console.log(tags.length);

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

      Exercise 3 · PracticeTurn a pair into a tuple

      TypeScript marks line 3 twice. Fix it by adding one type to line 1. What does the fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      const cakeLine = ["Cake", 4];const [name, price] = cakeLine;console.log(name.toUpperCase(), price + 1);

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

        Exercise 4 · PracticeType an empty array

        An empty cart, and two errors. What type should items have on line 2?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        function emptyCart() {  const items = [];  return items;}const cart = emptyCart();cart.push("tea");console.log(cart);

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

          Exercise 5 · ChallengeFind the cheapest without changing the cart

          TypeScript stops line 2, because the parameter is read-only. Fix the function without removing readonly. What does the fixed code print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          function cheapest(prices: readonly number[]) {  prices.sort((a, b) => a - b);  return prices[0];}const cart = [5, 3, 4];console.log(cheapest(cart), cart);

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

            Exercise 6 · ChallengeA breadcrumb that can't be empty

            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?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            type Breadcrumb = string[]; const trail: Breadcrumb = [];console.log(trail[0].toUpperCase());

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

              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.

              The arrays and tuples quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What type does TypeScript infer for these ratings?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const ratings = [4, 5, 3];

                Choose an answer to see the explanation.

              2. Question 2 of 7Without a type, what does TypeScript infer for this name-and-quantity pair?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const teaLine = ["Tea", 3];

                Choose an answer to see the explanation.

              3. Question 3 of 7Which line does TypeScript flag in this tuple code?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const cakeLine: [string, number] = ["Cake", 4];
                console.log(cakeLine[0]);
                console.log(cakeLine[2]);

                Choose an answer to see the explanation.

              4. Question 4 of 7What happens when this code reads past the end of an array?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const prices = [3, 4, 5];
                const fourth = prices[3];
                console.log(fourth);

                Choose an answer to see the explanation.

              5. Question 5 of 7Which call is allowed on tags: readonly string[]?

                Choose an answer to see the explanation.

              6. Question 6 of 7How many items can a [string, ...string[]] hold?

                Choose an answer to see the explanation.

              7. Question 7 of 7What does readonly add in const tags: readonly string[]?

                Choose an answer to see the explanation.

              Key takeaways

              • number[] and Array<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 like toSorted and map.

              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.

              CompleteFrontend Clear concepts. Working examples.