Arrays
Arrays are one of the most fundamental data structures in JavaScript, and TypeScript adds precise type information to every array — what types its elements can hold, whether it can be mutated, and more. This page covers everything from basic array types to advanced patterns like readonly arrays and tuple overlap.
Array Type Syntax
TypeScript offers two equivalent syntaxes for declaring array types. Both are correct; which you use is a matter of style.
// Syntax 1: element type followed by [] let numbers: number[] = [1, 2, 3]; let names: string[] = ['Alice', 'Bob', 'Carol']; let flags: boolean[] = [true, false, true]; // Syntax 2: Generic Array<T> form let numbers2: Array<number> = [1, 2, 3]; let names2: Array<string> = ['Alice', 'Bob']; // Both are interchangeable — pick one and stick with it // The T[] form is more common in idiomatic TypeScript
Array<T> form is needed in some contexts where T[] would be ambiguous — for example, inside JSX where < might be confused with a JSX tag. In non-JSX files, T[] is generally preferred.Type Inference for Arrays
TypeScript infers array types from their initial values, so explicit annotations are often unnecessary:
// Inferred as number[]
const scores = [95, 87, 92, 78];
// Inferred as string[]
const fruits = ['apple', 'banana', 'cherry'];
// Inferred as (string | number)[] — mixed types widen to a union
const mixed = [1, 'two', 3];
// TypeScript protects the array from wrong element types
scores.push(100); // OK
scores.push('A'); // Error: Argument of type 'string' is not assignable to type 'number'Empty Arrays Need Annotations
If you initialize an empty array without an annotation, TypeScript infers its type as never[] until you push something in. It is best to annotate empty arrays explicitly:
// Without annotation — inferred as never[]
const emptyBad = [];
emptyBad.push(1); // Error: Argument of type 'number' is not assignable to type 'never'
// With annotation — clear and safe
const empty: number[] = [];
empty.push(1); // OK
// Also valid
const empty2: Array<string> = [];
empty2.push('hello'); // OKCommon Array Methods — Typed
TypeScript knows the return types of every built-in array method. This means your callbacks get typed parameters automatically, and the return values are precise:
const numbers = [1, 2, 3, 4, 5];
// map: (value: number) => string → string[]
const strings = numbers.map((n) => n.toString());
// strings: string[]
// filter: keeps the same element type
const evens = numbers.filter((n) => n % 2 === 0);
// evens: number[]
// find: returns T | undefined (might not find anything)
const found = numbers.find((n) => n > 3);
// found: number | undefined
// reduce: TypeScript infers from initialValue
const sum = numbers.reduce((acc, n) => acc + n, 0);
// sum: number
// forEach: returns void
numbers.forEach((n) => {
console.log(n); // n: number
});reduce with a different accumulator type than the array element type, annotate the generic explicitly: numbers.reduce<string>((acc, n) => acc + n, "")Multidimensional Arrays
Nested arrays are expressed by stacking the [] suffix:
// 2D array — array of number arrays const matrix: number[][] = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ]; // Access is fully typed const row: number[] = matrix[0]; const cell: number = matrix[1][2]; // 6 // 3D array const cube: number[][][] = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]];
Arrays of Objects
Arrays commonly hold objects. Define the object shape with an interface or type alias, then use it as the element type:
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
const users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com', active: true },
{ id: 2, name: 'Bob', email: 'bob@example.com', active: false },
];
// TypeScript knows each element is a User
const firstUser = users[0]; // User
const firstName = firstUser.name; // string
// Array methods preserve the type
const activeUsers = users.filter((u) => u.active);
// activeUsers: User[]
const names = users.map((u) => u.name);
// names: string[]Union Element Types
An array can hold multiple types when you use a union as the element type:
// Array that can hold strings or numbers const mixed: (string | number)[] = [1, 'two', 3, 'four']; // Array that can hold null (useful for sparse data) const nullable: (string | null)[] = ['Alice', null, 'Carol', null]; // Filtering narrows the type const stringsOnly = mixed.filter((x): x is string => typeof x === 'string'); // stringsOnly: string[] — the type predicate narrows the result
(x): x is string syntax is a type predicate. It tells TypeScript that when the filter callback returns true, the element is a string. Without it, TypeScript would infer (string | number)[] even after filtering.readonly Arrays
readonly arrays prevent mutation — no push, pop, splice, or index assignment. Use them when you want to guarantee an array is never modified:
// Method 1: readonly prefix const readonlyNums: readonly number[] = [1, 2, 3]; // Method 2: ReadonlyArray<T> generic const readonlyNums2: ReadonlyArray<number> = [1, 2, 3]; // Method 3: as const (deepest immutability) const readonlyNums3 = [1, 2, 3] as const; // type: readonly [1, 2, 3] — a readonly tuple readonlyNums.push(4); // Error: Property 'push' does not exist on type 'readonly number[]' readonlyNums[0] = 99; // Error: Index signature in type 'readonly number[]' only permits reading
// Practical use: function that should not modify its input
function sum(numbers: readonly number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
// Both mutable and readonly arrays can be passed in
sum([1, 2, 3]); // OK
const locked: readonly number[] = [1, 2, 3];
sum(locked); // OK — readonly is compatiblereadonly arrays for function parameters when your function does not need to mutate the input. This signals intent clearly and prevents accidental mutations.Destructuring Arrays with Types
Destructuring works exactly as in JavaScript — TypeScript infers each variable's type from the array element type:
const rgb: [number, number, number] = [255, 128, 0]; // Destructuring a tuple — types are positional const [red, green, blue] = rgb; // red: number, green: number, blue: number // Destructuring a regular array const scores = [95, 87, 92]; const [first, second, ...rest] = scores; // first: number, second: number, rest: number[] // Default values const [a = 0, b = 0] = [42]; // a: number (42), b: number (0)
Spread and Rest with Typed Arrays
// Spread operator — works just like JS, types are merged
const a: number[] = [1, 2, 3];
const b: number[] = [4, 5, 6];
const combined: number[] = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
// Rest parameters — collect all remaining args into a typed array
function sumAll(...nums: number[]): number {
return nums.reduce((acc, n) => acc + n, 0);
}
sumAll(1, 2, 3, 4, 5); // 15
sumAll(); // 0 — empty rest param is valid
// Spread into a function call
const args: [number, number] = [10, 20];
Math.max(...args); // OKArray.from and Array Constructor
// Array.from — TypeScript infers the element type from the callback
const numbers = Array.from({ length: 5 }, (_, i) => i + 1);
// type: number[]
// value: [1, 2, 3, 4, 5]
// From a Set
const set = new Set([1, 2, 3, 2, 1]);
const unique = Array.from(set);
// type: number[]
// value: [1, 2, 3]
// From a string (produces string[])
const chars = Array.from('hello');
// type: string[]
// value: ['h', 'e', 'l', 'l', 'o']Sorting Typed Arrays
// sort mutates the array and returns it
const nums: number[] = [3, 1, 4, 1, 5, 9];
// Always provide a comparator for number arrays
// Without one, JS converts to strings: [1, 1, 3, 4, 5, 9] (happens to be correct here)
// But [10, 9, 20] would sort as [10, 20, 9] without a comparator
nums.sort((a, b) => a - b); // ascending
nums.sort((a, b) => b - a); // descending
// Sorting objects
interface Product {
name: string;
price: number;
}
const products: Product[] = [
{ name: 'C', price: 30 },
{ name: 'A', price: 10 },
{ name: 'B', price: 20 },
];
products.sort((a, b) => a.price - b.price);
// Sorted by price ascending
products.sort((a, b) => a.name.localeCompare(b.name));
// Sorted alphabetically by nameArray.prototype.sort mutates the original array in place. If you need a sorted copy, spread first: [...original].sort(...). ES2023 added a non-mutating toSorted() method.Typed Array Methods — Return Types Table
Method | Input | Return Type |
|---|---|---|
map(fn) | T[] | U[] (inferred from fn) |
filter(fn) | T[] | T[] |
filter(predicate) | T[] | U[] (narrowed by predicate) |
find(fn) | T[] | T | undefined |
findIndex(fn) | T[] | number |
reduce(fn, init) | T[] | typeof init |
some(fn) | T[] | boolean |
every(fn) | T[] | boolean |
includes(val) | T[] | boolean |
indexOf(val) | T[] | number |
flat() | (T | T[])[] | T[] |
flatMap(fn) | T[] | U[] |
Practical Pattern: Processing an Array of API Results
interface ApiResponse {
id: number;
title: string;
completed: boolean;
userId: number;
}
async function fetchTodos(): Promise<ApiResponse[]> {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
return response.json() as Promise<ApiResponse[]>;
}
async function main() {
const todos: ApiResponse[] = await fetchTodos();
// Filter to completed todos — still ApiResponse[]
const done = todos.filter((t) => t.completed);
// Extract titles — string[]
const titles = done.map((t) => t.title);
// Group by userId — Record<number, ApiResponse[]>
const byUser = todos.reduce<Record<number, ApiResponse[]>>((acc, todo) => {
const key = todo.userId;
if (!acc[key]) acc[key] = [];
acc[key].push(todo);
return acc;
}, {});
}Summary
Use T[] or Array<T> syntax — both are equivalent, T[] is more common
TypeScript infers array types from initial values — annotate empty arrays explicitly
All built-in array methods return precise types — map, filter, find, reduce all work as expected
readonly number[] prevents mutation — use for function parameters that should not modify input
as const creates a deeply readonly tuple with narrowest literal types
Type predicates in filter callbacks narrow the resulting array type
Always provide a comparator to .sort() when sorting numbers or objects
Use Array.from() with a mapping function to create typed arrays of any length