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 typeT | 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.
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 hold02The 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.
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 numbers04Reading 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.
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.
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
Make the index safe
In the second example change line 2 to
const first = colors[0] ?? "none";and read the new type. It becomesstring, and the error on line 4 is gone. - 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 aroundstring | numberand it passes. - 3
Turn the flag off
In a real project, set
"noUncheckedIndexedAccess": falseintsconfig.json. With it off,colors[0]would be plainstringandcolors[10].toUpperCase()would type-check, then crash at runtime. The flag's whole job is to stop that.
Code & diagrams
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 ] 5noUncheckedIndexedAccess 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
4Break 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.
Break #2
Add the wrong kind to a typed list
In the first example, a.push("five") pushes text into a 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 narrowlist[0], so useat(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.tsand newer libs.filterwith a type predicate narrows the item type (Topic 4.7),flatandflatMapcompute depth with conditional types, andat()needslibES2022 or newer.
Remember this
- 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 innumber[]orstring[], and **Array<T>**, as inArray<string>. They mean exactly the same. Most code usesT[]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 writeArray<{ x: number }>. - 2
TypeScript infers array types from their contents:
[true, false]isboolean[]. 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, whilestring | number[]is either one string, or a list of numbers. - 3
Once the item type is known, every method is checked.
numbers.push(4)is fine,numbers.push("five")isTS2345, andnumbers.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
Reading by position is the one place the type lies a little.
colors[10]on a three-item array isundefinedat runtime, but plain TypeScript types it asstring. The flag **noUncheckedIndexedAccess** (not part ofstrict, but turned on in thetsc --initfile this course uses) fixes it: every read by index becomesT | 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
How to deal with
T | undefinedfrom an index: check it (if (first !== undefined) { ... }), give a fallback with??(Topic 1.7:colors[0] ?? "none"), or useat(0)/findstyle methods that are honest about missing items in their types. Loops are the exception:for (const color of colors)yieldsstring, notstring | undefined, because the loop only visits items that exist. - 6
**
readonlyarrays, a first look.**readonly number[]is the same list, but with the changing methods (push,pop,sortin place, assigning by index) taken away. The compiler then rejectspushandfixed[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
What is the difference between number[] and Array<number>?
What does noUncheckedIndexedAccess do and why is it not in strict?
What is the difference between (string | number)[] and string | number[]?
What does readonly string[] guarantee, and what does it not?
Practice
Create an empty list ids that will hold numbers, push two ids, then print the first id doubled. Make it compile with noUncheckedIndexedAccess on.
Write lastOf(list: string[]): string that returns the last item, or "none" for an empty list. It must compile.
Trade-offs
- ↔
noUncheckedIndexedAccessadds small checks in index-heavy code (matrices, parsing), but turns many runtime crashes into compile errors. Teams that dislike it usually switch tofor...of,mapandat(). - ↔
Using
readonly T[]for function parameters makes functions safer and more flexible (they accept bothT[]andreadonly 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]isT | undefinedwithnoUncheckedIndexedAccess.I can handle a maybe-missing item with
??or anifcheck.I can say what
readonlystops and what it doesn't.