Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~20 min· topic 4 of 7

Topic 1.4

Arrays

In one line

An array type says what every item is: number[] or Array<number>. With noUncheckedIndexedAccess, reading by position gives T | undefined, because TypeScript can't know the position exists.

Think of it like this

A train where every coach has the same kind of seat. A "number train" only carries numbers. If you try to put a banana in coach 3, the guard stops you. An array type is the rule for what each coach holds, and it does not say how many coaches there are: the train can be long or even empty.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Array
An ordered list of values, reached by position (index) starting at 0.
T[]
Array type notation: "array of T". number[] is a list of numbers.
Array<T>
The same type written as a generic. Identical to T[].
Index
The position of an item. list[0] is the first.
Union
A | B: either kind. (string | number)[] is a list whose items are strings or numbers.
undefined
The value you get when you read something that isn't there, like list[99].
noUncheckedIndexedAccess
A compiler flag that makes list[i] have the type T | undefined.
readonly
A marker that stops code from changing a value through that name.
Tuple
A fixed-length array with a type for each position (Topic 5.2).

Step by step

01Two spellings of one type

number[] and Array<number> are identical. Pick T[] unless the item type is complicated.

TypeScript infers the type from the items. You only annotate when the array starts empty.

arrays.tswhole filets
const a: number[] = [1, 2, 3];
const b: Array<string> = ["x", "y"];
const c = [true, false];       // boolean[]
const names: string[] = [];    // empty: say what it will hold

02The item type is enforced

Every method that adds or reads items is checked against the item type. Adding text to a number list is TS2345.

This is the same check as in Topic 1.1: the argument must fit the parameter. push has a parameter of type number for a number[].

03Mixed arrays are unions

A list holding numbers, strings and null is (string | number | null)[]. Parentheses matter: without them the type means something else.

Reading an item from a mixed list gives a union, which limits what you can do with it until you narrow it.

mixed.tswhole filets
const d = [1, "two", null];
// (string | number | null)[]

let x: (string | number)[];   // a list of mixed items
let y: string | number[];     // one string OR a list of numbers

04Reading by position can be undefined

A three-item list has no item at position 10. JavaScript hands back undefined, and a method call on it crashes.

With noUncheckedIndexedAccess the type tells the truth: T | undefined. The checker then forces you to handle it before use.

Reading by position can be undefineddiagram
Rendering diagram…

05Handling the maybe-missing item

Three honest ways: an if check, a ?? fallback, or a method whose type already says it may find nothing, such as at().

for...of is the safe way to visit every item; it never produces undefined.

safe-read.tswhole filets
const colors = ["red", "green", "blue"];

const first = colors[0] ?? "none";   // string
const maybe = colors.at(5);          // string | undefined

for (const color of colors) {
  console.log(color.toUpperCase()); // color is string
}

06readonly: a list you can't change

readonly number[] removes the methods that change the list. push is not found on the type and index assignment is refused.

Use it for parameters, to promise a function won't alter the caller's list. There is more in Topic 5.1.

Try it yourself

  1. 1

    Make the index safe

    In the second example change line 2 to const first = colors[0] ?? "none"; and read the new type. It becomes string, and the error on line 4 is gone.

  2. 2

    Parentheses change the meaning

    Add let x: string | number[] = ["a"]; to the first example. Predict whether it is an error. It is: the type means one string or a list of numbers, and ["a"] is a list of strings. Add parentheses around string | number and it passes.

  3. 3

    Turn the flag off

    In a real project, set "noUncheckedIndexedAccess": false in tsconfig.json. With it off, colors[0] would be plain string and colors[10].toUpperCase() would type-check, then crash at runtime. The flag's whole job is to stop that.

Code & diagrams

Array types and the item check

The push of five is rejected, but the console shows it was added anyway: the array now holds a string where numbers were promised. This is the bug the check prevents.

Console

(nothing was printed)

Expected console output

[ 1, 2, 3, 4, 'five' ] [ 'x', 'y', 'z' ] [ true, false ] [ 1, 'two', null ] 5
Index reads, undefined, and readonly

noUncheckedIndexedAccess is on in these examples (it is in the tsc --init file), so colors[0] is string | undefined even though it exists. The two readonly lines are errors, yet the JavaScript runs them: the final length is 4, because readonly is only a compile-time promise.

Console

(nothing was printed)

Expected console output

RED
undefined
4

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use an indexed item without checking it

In the second example, first.toUpperCase() uses colors[0] straight away.

terminal
$ Types panel
── what you'll see ──
index.ts(4,13): error TS18048: 'first' is possibly 'undefined'.

Break #2

Add the wrong kind to a typed list

In the first example, a.push("five") pushes text into a number[].

terminal
$ Types panel
── what you'll see ──
index.ts(8,8): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

Myth vs fact

Myth

T[] and Array<T> behave differently.

Fact

They are the same type written two ways. Style guides pick one; the compiler does not care.

Myth

An array type includes its length.

Fact

A number[] can have any number of items, including none. Fixed lengths need a tuple (Topic 5.2).

Myth

readonly makes the array frozen at runtime.

Fact

readonly is erased. It only makes the compiler reject changes through that name; another reference can still change the same list.

Myth

noUncheckedIndexedAccess is part of strict.

Fact

It is not. It is a separate flag that tsc --init turns on in this course's configuration; without it, list[i] is typed as just T.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Without noUncheckedIndexedAccess, indexing is unsound on purpose: the language can't track length, and checking every read would annoy people more than it helps. The flag trades that convenience for safety. After a length check (if (list.length > 0)) TypeScript still does not narrow list[0], so use at(0) or destructuring with a default.

  • ▸

    Arrays are covariant in TypeScript: a string[] is accepted where (string | number)[] is expected, though pushing a number through the wider name would corrupt the original. Topic 16.5 lists this among the deliberate holes.

  • ▸

    Array methods are typed with generics and overloads in lib.es5.d.ts and newer libs. filter with a type predicate narrows the item type (Topic 4.7), flat and flatMap compute depth with conditional types, and at() needs lib ES2022 or newer.

Remember this

  1. 1

    The problem this solves: A list can quietly collect the wrong kind of item, such as text among prices, and the sum later comes out wrong. An array type fixes what every item must be. An array is an ordered list of values you reach by position, starting at 0. You write its type in one of two equal ways: **T[]** (read "array of T"), as in number[] or string[], and **Array<T>**, as in Array<string>. They mean exactly the same. Most code uses T[] because it is shorter. Array<T> is a generic type (a type with a hole for another type, Phase 7), and it is useful when the item type is long or when you write Array<{ x: number }>.

  2. 2

    TypeScript infers array types from their contents: [true, false] is boolean[]. An array mixing kinds is the union of them: [1, "two", null] is (string | number | null)[], an array whose every item is one of those. The brackets matter: (string | number)[] is a list of mixed items, while string | number[] is either one string, or a list of numbers.

  3. 3

    Once the item type is known, every method is checked. numbers.push(4) is fine, numbers.push("five") is TS2345, and numbers.map(...) gives back an array of whatever the callback returns. Mixed arrays are legal but awkward: reading an item gives you the union, so you have to check which kind it is before you can use kind-specific methods (Topic 4.3). When the position of each kind is fixed, such as [name, age], a tuple is the better tool (Topic 5.2).

  4. 4

    Reading by position is the one place the type lies a little. colors[10] on a three-item array is undefined at runtime, but plain TypeScript types it as string. The flag **noUncheckedIndexedAccess** (not part of strict, but turned on in the tsc --init file this course uses) fixes it: every read by index becomes T | undefined, so you must handle the empty case. The cost is a few extra checks; the benefit is that a whole class of "cannot read properties of undefined" crashes can no longer hide.

  5. 5

    How to deal with T | undefined from an index: check it (if (first !== undefined) { ... }), give a fallback with ?? (Topic 1.7: colors[0] ?? "none"), or use at(0) / find style methods that are honest about missing items in their types. Loops are the exception: for (const color of colors) yields string, not string | undefined, because the loop only visits items that exist.

  6. 6

    **readonly arrays, a first look.** readonly number[] is the same list, but with the changing methods (push, pop, sort in place, assigning by index) taken away. The compiler then rejects push and fixed[0] = 9. It protects a list you hand to someone else from being changed. It is checked only at compile time, and it is shallow, covered in Topic 5.1.

Explain it without notes

01

What is the difference between number[] and Array<number>?

02

What does noUncheckedIndexedAccess do and why is it not in strict?

03

What is the difference between (string | number)[] and string | number[]?

04

What does readonly string[] guarantee, and what does it not?

Practice

01

Create an empty list ids that will hold numbers, push two ids, then print the first id doubled. Make it compile with noUncheckedIndexedAccess on.

02

Write lastOf(list: string[]): string that returns the last item, or "none" for an empty list. It must compile.

Trade-offs

  • ↔

    noUncheckedIndexedAccess adds small checks in index-heavy code (matrices, parsing), but turns many runtime crashes into compile errors. Teams that dislike it usually switch to for...of, map and at().

  • ↔

    Using readonly T[] for function parameters makes functions safer and more flexible (they accept both T[] and readonly T[]), at the cost of an extra word in every signature.

Done when you can

  • I can write an array type two ways and say they are the same.

  • I can predict the type of a mixed array and write (A | B)[] correctly.

  • I can explain why list[i] is T | undefined with noUncheckedIndexedAccess.

  • I can handle a maybe-missing item with ?? or an if check.

  • I can say what readonly stops and what it doesn't.