Arrays and Tuples
Homogeneous collections and fixed-length sequences with precise type tracking
JavaScript arrays are wonderfully flexible — you can mix numbers,
strings, objects, even other arrays in a single list. But this
flexibility is also a liability: if you expect an array of numbers and
accidentally receive an array of strings, JavaScript won't complain until
you try to do math and get NaN.
TypeScript introduces typed arrays and tuples to bring order to the chaos. An array type says "this is a list of things, all of the same type." A tuple type says "this is a sequence of a fixed length, where each position has its own type." Together, they let the compiler verify that you're not accidentally mixing incompatible data or indexing beyond the bounds of a known-length sequence.
This chapter answers: How do you type arrays? What's the difference between arrays and tuples? What bugs does this prevent?
Array types: homogeneous collections
The simplest array type is T[], meaning "an array whose elements are
all of type T." For example, number[] is an array of numbers,
string[] is an array of strings:
What the compiler now knows: Every element of numbers is a
number. If you write numbers.push("six"), the compiler rejects it —
"six" is not a number. The type annotation acts as a contract: "this
array holds only numbers."
TypeScript also provides a generic Array<T> syntax, which is
functionally identical to T[]:
The two syntaxes are interchangeable. Most codebases prefer T[] for
simple types (it's more concise), but Array<T> can be clearer when T
itself is complex (e.g., Array<Promise<User>>).
Type inference for arrays
If you initialize an array with values, TypeScript infers the element type:
Notice the last case: mixed is inferred as (number | string)[],
meaning each element is either a number or a string. This is correct —
the array holds both — but it also means you lose precision. If you index
into mixed, TypeScript knows the result is number | string, not
specifically number. You'll need to narrow the type before you can do
math with it (we'll cover narrowing in Type
Narrowing).
Readonly arrays
By default, TypeScript arrays are mutable: you can push, pop,
splice, etc. But sometimes you want to express that an array should not
be modified. Enter readonly:
What bugs does this prevent? If you pass an array to a function that
you expect not to modify it, declaring the parameter as readonly T[]
makes that contract explicit. The compiler ensures that the function
doesn't accidentally call a mutating method. This is especially valuable
in large codebases where function signatures act as machine-checked
documentation.
Alternatively, you can use the ReadonlyArray<T> generic:
readonly T[] and ReadonlyArray<T> are equivalent; choose whichever
reads better to you.
Tuples: fixed-length, heterogeneous sequences
An array type says "a list of some length, all elements of type T." A
tuple type says "a sequence of exactly N elements, where each
position has its own type." For example, [string, number] is a
two-element tuple: the first element is a string, the second is a
number:
What the compiler now knows: person[0] is specifically a string,
not string | number. person[1] is specifically a number. If you
try to assign person = [30, "Alice"], the compiler rejects it — the
order matters.
Tuples shine when a function returns multiple values:
Without tuples, you'd have to return an object with named properties
({ quotient, remainder }). Tuples are lighter-weight when the structure
is simple and the positions are self-explanatory.
Named tuple elements
TypeScript 4.0+ allows you to label tuple elements, which improves readability without changing the runtime behavior:
The labels appear in editor tooltips and error messages, making the code self-documenting. But you still access elements by index, not by name.
Optional tuple elements
You can mark tuple elements as optional with ?:
Optional elements must come after required elements (just like optional
function parameters). The type of an optional element is T | undefined.
Rest elements in tuples
A tuple can have a rest element (...T[]) to represent a variable
number of trailing elements:
The rest element must be last. It behaves like a normal array, so you can
iterate over it, push to it (if the tuple is mutable), etc.
Variadic tuple types (a glimpse)
TypeScript 4.0+ supports variadic tuple types, which let you parameterize the length and element types of a tuple. This is an advanced feature (we'll revisit it when we cover generics), but here's a taste:
The return type [T, ...U] says "a tuple that starts with a T, then
includes all the elements of U." The compiler tracks the entire
structure. This level of precision is what makes TypeScript's tuple types
so powerful.
Tuples vs arrays: when to use which
Use arrays (T[]) when:
- The collection can grow or shrink at runtime
- All elements have the same type and role
- You'll iterate over the entire collection
Use tuples when:
- The length is fixed and known at compile time
- Each position has a distinct type or meaning
- You want destructuring to give precise types
A common pattern: a function returns a tuple (e.g., [error, data] or
[status, response]), and the caller destructures it to get strongly
typed pieces.
Practice: a multi-file coordinate system
Let's solidify these concepts with a challenge that spans two files. You'll define a tuple type for 3D coordinates in one file and use it in another.
You have two files: types.ts defines a Point3D tuple type (a 3-element tuple of numbers representing x, y, z). In main.ts, implement the function distance(a: Point3D, b: Point3D): number that returns the Euclidean distance between two points. The formula is sqrt((x2-x1)² + (y2-y1)² + (z2-z1)²). The test checks the distance between [0,0,0] and [3,4,0] (which is 5).
Common pitfalls: array vs tuple length
TypeScript's array type does not track length. If you declare numbers: number[], the compiler doesn't know if the array has 0, 1, or 100
elements. Indexing numbers[0] gives number | undefined (in strict
mode), because the array might be empty. Tuples, by contrast, do track
length: if you declare pair: [number, number], the compiler knows
pair[0] and pair[1] exist and are numbers (no | undefined).
This is a subtle but important distinction: tuples encode length in the type, arrays do not.
Check your understanding
What is the type of the variable x after this declaration: const x = [1, "two", true];?
any[]
Array<number | string | boolean>
(number | string | boolean)[]
[number, string, boolean] (tuple)
Which of the following correctly represents a tuple of a string and a number, in that order?
[number, string]
[string, number]
{ 0: string, 1: number }
string | number
Summary
Arrays and tuples give you two ways to type collections. Arrays (T[])
represent homogeneous lists of any length; tuples ([T, U, ...])
represent heterogeneous sequences of fixed length. Both can be made
readonly to prevent mutation. The compiler uses these types to catch
bugs: indexing beyond tuple bounds, pushing the wrong type into an array,
accidentally mutating a collection you meant to keep immutable.
In the next chapter, Objects and Interfaces, we'll see how to type structured data — objects with named properties, each with its own type.