TypeScript Arrays
Arrays in TypeScript are the same JavaScript arrays you already know, but with a type layered on top so the compiler can guarantee what kind of values live inside them. Instead of discovering at runtime that a string snuck into a list of numbers, TypeScript catches the mistake the moment you write the code. This lesson covers how array types are written, how tuples and readonly arrays refine that idea further, and the traps that catch even experienced developers.
Overview / How it works
A TypeScript array type describes two things: that the value is an array, and what type its elements are allowed to be. The two equivalent ways to write this are the shorthand T[] and the generic form Array<T> — number[] and Array<number> mean exactly the same thing to the compiler. Most style guides (and this site) prefer the shorthand for simple element types and reserve the generic form for readability in more complex generic code.
By default, TypeScript enforces homogeneous arrays: every element must be assignable to the declared element type. If you need an array that legitimately holds more than one type, you type it with a union, such as (string | number)[] — note the parentheses, because string | number[] parses as “a string, or an array of numbers,” not “an array of strings and numbers.”
TypeScript can also infer array types without an annotation. Given const nums = [1, 2, 3], the compiler infers number[]. Given a mixed literal like const mixed = [1, "two"], it infers the union (string | number)[] automatically — this is called the “best common type” algorithm. Whether you annotate or let inference do the work, the guarantee is the same: everywhere that array is used, only compatible values can be pushed, assigned, or read.
Crucially, none of this exists at runtime. Type annotations are a compile-time-only construct that TypeScript strips away entirely during compilation — the JavaScript that actually runs has no idea an array was ever “typed,” it’s just a plain array. This is called type erasure, and it’s why TypeScript can add powerful compile-time guarantees with zero runtime cost.
Syntax
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
let list3: readonly string[] = ["a", "b"];
let tuple1: [string, number] = ["age", 30];
let matrix: number[][] = [[1, 2], [3, 4]];
| Form | Meaning |
|---|---|
T[] |
Shorthand array type — an array whose elements are all type T. |
Array<T> |
Generic form, identical meaning to T[]. |
readonly T[] / ReadonlyArray<T> |
An array whose contents cannot be mutated through this reference (no push, pop, splice, or index assignment). |
[T, U] |
A tuple — a fixed-length array where each position has its own declared type. |
T[][] |
An array of arrays, used for grids or matrices; can be nested further as T[][][], etc. |
Examples
Example 1: Typed arrays and array methods
let temperatures: number[] = [72, 68, 75, 80];
let cities: Array<string> = ["Tokyo", "Paris", "Cairo"];
temperatures.push(90);
// temperatures.push("hot"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
function average(nums: number[]): number {
const total = nums.reduce((sum, n) => sum + n, 0);
return total / nums.length;
}
console.log(temperatures);
console.log(cities);
console.log(average(temperatures));
Output:
[ 72, 68, 75, 80, 90 ]
[ 'Tokyo', 'Paris', 'Cairo' ]
77
The number[] annotation means every array method — push, reduce, map, and so on — is type-checked against number. The commented-out line would fail to compile because "hot" is not assignable to number; TypeScript checks this before the code ever runs.
Example 2: Tuples and readonly arrays
let point: [number, number] = [10, 20];
let entry: [string, number, boolean] = ["temperature", 72, true];
const primes: readonly number[] = [2, 3, 5, 7, 11];
function sumTuple([x, y]: [number, number]): number {
return x + y;
}
console.log(point);
console.log(entry);
console.log(primes);
console.log(sumTuple(point));
Output:
[ 10, 20 ]
[ 'temperature', 72, true ]
[ 2, 3, 5, 7, 11 ]
30
A tuple like [string, number, boolean] pins down not just the element types but their order and count — position 0 must be a string, position 1 a number, position 2 a boolean. This makes tuples ideal for fixed-shape data like coordinate pairs or React’s useState return value. The readonly number[] annotation on primes means the compiler blocks any attempt to mutate it later, even though the underlying array is otherwise ordinary.
Example 3: Arrays of objects and multidimensional arrays
interface Product {
name: string;
price: number;
inStock: boolean;
}
const inventory: Product[] = [
{ name: "Keyboard", price: 49.99, inStock: true },
{ name: "Mouse", price: 19.99, inStock: false },
{ name: "Monitor", price: 199.99, inStock: true },
];
const availableTotal: number = inventory
.filter((product) => product.inStock)
.reduce((sum, product) => sum + product.price, 0);
const grid: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
const middleRow: number[] = grid[1];
console.log(availableTotal);
console.log(middleRow);
console.log(grid[2][0]);
Output:
249.98
[ 4, 5, 6 ]
7
Typing inventory as Product[] means every callback passed to filter and reduce gets its parameter automatically typed as Product — no manual annotation needed inside the lambda, because TypeScript infers it from the array’s element type. The number[][] annotation on grid shows how multidimensional arrays are just arrays of arrays; indexing twice (grid[2][0]) walks down one dimension at a time.
How it works step by step / Under the hood
- Inference from literals: when you write an array literal without an annotation, TypeScript scans every element and computes the “best common type” — a single type if all elements match, or a union if they don’t.
- Evolving arrays: for
let arr = []with no annotation, TypeScript defers the element type and lets it “evolve” as you callpushwithin the same scope — the final inferred type is the union of everything pushed. This is a narrow special case for uninitializedlet/vararrays, not a general escape hatch from type checking. - Structural checking on assignment: assigning one array to a variable of another array type checks element-type compatibility structurally, the same rules used everywhere else in TypeScript (e.g. an array of an object type is assignable to an array of a compatible interface, even without an explicit relationship between the two).
- Tuples are compile-time only: at runtime a tuple is just a regular JavaScript array with no special length or shape enforcement built in — TypeScript’s checker is the only thing preventing you from pushing extra elements onto it through typed code.
- Erasure: after compilation, every array-related type annotation disappears. The compiled JavaScript for
let nums: number[] = [1, 2, 3]is simplylet nums = [1, 2, 3];— there is no runtime check that stops you from mutating an array incorrectly through untyped orany-typed code that bypasses the compiler. - Index access: by default,
arr[i]is typed as the element typeT, notT | undefined, even though an out-of-range index genuinely returnsundefinedat runtime. The compiler flagnoUncheckedIndexedAccesschanges this by making every index access returnT | undefined, which is safer but requires extra narrowing at call sites.
Common Mistakes
Mistake 1: Leaving an array untyped and losing type safety
let scores = [];
scores.push(90);
scores.push("ninety");
console.log(scores);
Output:
[ 90, 'ninety' ]
This compiles without error because of the “evolving array” behavior described above — without an annotation, scores silently accepts both a number and a string. That defeats the entire purpose of typing the array. The fix is to annotate the element type up front:
let scores: number[] = [];
scores.push(90);
// scores.push("ninety"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
console.log(scores);
Output:
[ 90 ]
Mistake 2: Trying to mutate a readonly array
function archiveScores(scores: readonly number[]): void {
scores.push(0);
console.log(scores);
}
This fails to compile with “Property ‘push’ does not exist on type ‘readonly number[]’.” Marking a parameter readonly is a promise to callers that their array won’t be mutated, so TypeScript removes every mutating method (push, pop, splice, sort without a copy, index assignment) from its type. If you need a modified copy, build a new array instead:
function archiveScores(scores: readonly number[]): number[] {
const archived = [...scores, 0];
console.log(archived);
return archived;
}
archiveScores([10, 20, 30]);
Output:
[ 10, 20, 30, 0 ]
Best Practices
- Always annotate empty arrays you intend to fill (
let ids: string[] = []) rather than relying on evolving-array inference. - Prefer
T[]overArray<T>for simple element types; save the generic form for cases where it reads more clearly (e.g. nested generics). - Mark function parameters
readonly T[]when the function only reads the array — it documents intent and prevents accidental mutation. - Use tuples for genuinely fixed-shape data (coordinates, key/value pairs, RGB triples), not as a substitute for an object with named properties.
- Avoid
any[]; if elements can legitimately be more than one type, use an explicit union like(string | number)[]so the compiler still checks something. - Enable
noUncheckedIndexedAccessin stricter projects so indexing an array forces you to handle the possibility ofundefined. - When an array’s elements are objects, define an
interfaceortypefor the element and type the array asElement[]rather than typing every literal inline.
Practice Exercises
- Write a function
sumEven(nums: number[]): numberthat returns the sum of only the even numbers in the array. Test it with[1, 2, 3, 4, 5, 6]and confirm the expected output is12. - Declare a tuple type
type RGB = [number, number, number]and write a functiontoCss(color: RGB): stringthat returns a string like"rgb(255, 0, 0)". - Given
interface Task { title: string; done: boolean }, write a functionpendingTitles(tasks: readonly Task[]): string[]that returns the titles of tasks wheredoneisfalse, without mutating the input array.
Summary
T[]andArray<T>are equivalent ways to type a homogeneous array.- TypeScript infers array types from literals using a “best common type”/union algorithm, and can “evolve” the type of an untyped
letarray — always annotate empty arrays to avoid this trap. - Tuples (
[T, U, ...]) add fixed length and per-position typing on top of a normal array, but that guarantee is compile-time only. readonly T[]removes mutating methods from the type, documenting and enforcing read-only usage.- Multidimensional arrays are just arrays of arrays, typed as
T[][]and indexed one dimension at a time. - All array typing is erased at compile time — the emitted JavaScript is a plain array with no runtime type checks, so type-only guarantees never protect against untyped or
any-typed code paths.
