Skip to content
TypeScript ts basic-types 5 min read

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

FormMutableMeaningUse when
T[]YesShorthand array of TDefault for simple elements
Array<T>YesGeneric array of TComplex element types
readonly T[]NoImmutable arrayRead-only parameters/returns
ReadonlyArray<T>NoGeneric immutable arraySame as above, generic style
(A | B)[]YesArray of a unionHeterogeneous collections

Enable noUncheckedIndexedAccess in tsconfig.json to have indexed access return T | undefined. It catches a whole class of “array might be empty” bugs at the cost of a few extra guards.

Best Practices

  • Pick T[] or Array<T> as a team convention and apply it consistently.
  • Always annotate empty arrays (const x: number[] = []) to avoid the any[] pitfall.
  • Use readonly T[] for parameters and returns that should not be mutated.
  • Parenthesize union element types: write (string | number)[], not string | number[].
  • Let TypeScript infer the element type from non-empty literals instead of annotating.
  • Reach for as const when you need a fixed-length, literal-typed, immutable array.
  • Turn on noUncheckedIndexedAccess to surface out-of-bounds access at compile time.
Last updated June 29, 2026
Was this helpful?