Arrays
Arrays hold ordered, same-typed collections, and TypeScript tracks the element type so every read and write stays type-safe. You can write an array type two equivalent ways, restrict elements to a union, mark an array as immutable, and nest arrays for grids and matrices. TypeScript also infers array types from literals automatically, which is convenient but has one well-known pitfall with empty arrays. This page covers the syntax, the inference rules, and the immutability tools you reach for in everyday code.
T[] and Array<T>
There are two interchangeable ways to spell an array type: the shorthand T[] and the generic form Array<T>. They compile to exactly the same type, so the choice is purely stylistic.
const scores: number[] = [98, 72, 85];
const names: Array<string> = ["Ada", "Grace"];
scores.push(100); // ✅
// ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'.
scores.push("oops");
Most teams prefer T[] for simple element types because it reads cleanly, and reserve Array<T> when the element type is itself complex (for example Array<() => void>) where the brackets would be hard to scan. Pick one convention and stay consistent across a codebase.
Arrays of unions
When elements can be more than one type, give the element a union and wrap it in parentheses before the brackets. The parentheses matter: (string | number)[] is an array of either, while string | number[] is a string or an array of numbers.
const mixed: (string | number)[] = [1, "two", 3];
// Different meaning entirely:
let either: string | number[]; // a string OR a number[]
either = "hello"; // ✅
either = [1, 2, 3]; // ✅
// ❌ Error: Type 'number' is not assignable to type 'string | number[]'.
either = 42;
Each element of mixed is checked against the whole union, so both 1 and "two" are valid. Narrow union elements with typeof checks before using type-specific methods. See Union Types for the narrowing patterns.
Readonly arrays
A readonly T[] (or the equivalent ReadonlyArray<T>) produces an array whose elements cannot be reassigned and whose mutating methods like push, pop, and splice are removed from the type. Use it to signal that a collection must not be modified.
function sum(values: readonly number[]): number {
// ❌ Error: Property 'push' does not exist on type 'readonly number[]'.
// values.push(0);
return values.reduce((a, b) => a + b, 0);
}
const data: ReadonlyArray<number> = [1, 2, 3];
sum(data); // ✅
A mutable number[] is assignable to a readonly number[] parameter, but not the reverse — you cannot pass a readonly array where a mutable one is expected. Prefer readonly parameters for functions that only read, as it documents intent and prevents accidental mutation. Related: Readonly Properties.
Multidimensional arrays
Nest the bracket syntax to model grids and matrices. number[][] is an array of number[] rows, and you can add further dimensions as needed.
const grid: number[][] = [
[1, 2, 3],
[4, 5, 6],
];
const cell: number = grid[0][1]; // 2
const cube: number[][][] = [[[0]]];
Each level of brackets adds a dimension. Indexing returns the next-inner type — grid[0] is a number[], and grid[0][1] is a number. Note that TypeScript does not check array bounds, so out-of-range access is typed as the element type, not undefined, unless noUncheckedIndexedAccess is enabled.
Inference from literals
When you initialize an array with a literal, TypeScript infers the element type by forming a union of the values. You rarely need an explicit annotation for a populated literal.
const flags = [true, false]; // inferred: boolean[]
const mixed = [1, "two"]; // inferred: (string | number)[]
// `as const` narrows to a readonly tuple of literal types:
const point = [10, 20] as const; // readonly [10, 20]
The first two examples show normal widening to boolean[] and (string | number)[]. Adding as const instead produces a deeply readonly tuple of literal types, which is covered in depth on Const Assertions and Tuples.
The empty-array any[] pitfall
An empty array literal with no annotation starts as the implicit any[] “evolving” type. In non-strict configs it silently becomes any, defeating type checking. Always annotate empty arrays you intend to fill.
// ❌ Risky: type is any[] and grows untyped
const items = [];
items.push(1);
items.push("two"); // no error — type safety lost
// ✅ Annotate up front
const ids: number[] = [];
ids.push(1);
// ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'.
ids.push("two");
The first array has no values to infer from, so TypeScript cannot determine the element type. The annotated version locks in number[] immediately, restoring full checking on every push.
Array type forms compared
| Form | Mutable | Meaning | Use when |
|---|---|---|---|
T[] | Yes | Shorthand array of T | Default for simple elements |
Array<T> | Yes | Generic array of T | Complex element types |
readonly T[] | No | Immutable array | Read-only parameters/returns |
ReadonlyArray<T> | No | Generic immutable array | Same as above, generic style |
(A | B)[] | Yes | Array of a union | Heterogeneous collections |
Enable
noUncheckedIndexedAccessintsconfig.jsonto have indexed access returnT | undefined. It catches a whole class of “array might be empty” bugs at the cost of a few extra guards.
Best Practices
- Pick
T[]orArray<T>as a team convention and apply it consistently. - Always annotate empty arrays (
const x: number[] = []) to avoid theany[]pitfall. - Use
readonly T[]for parameters and returns that should not be mutated. - Parenthesize union element types: write
(string | number)[], notstring | number[]. - Let TypeScript infer the element type from non-empty literals instead of annotating.
- Reach for
as constwhen you need a fixed-length, literal-typed, immutable array. - Turn on
noUncheckedIndexedAccessto surface out-of-bounds access at compile time.