Almost every program you ever write will need to hold a list of things: a list of usernames, a list of prices, a list of points on a chart. In plain JavaScript, you just throw values into an array and hope you remember what’s inside. TypeScript asks one extra question that saves you from a lot of bugs: what kind of values does this list hold, and how many?
That single question splits into two tools. An array is a list that can grow and shrink, where every item is the same kind of thing. A tuple is a fixed-size slot where each position has its own meaning and its own type. They look similar in code, but they solve very different problems. By the end of this article you’ll know exactly which one to reach for, and how to lock either of them down so nobody accidentally changes data they shouldn’t.
Typing a simple array
Let’s start with the everyday case: a list where every item shares the same type. In TypeScript you describe that by writing the item’s type followed by square brackets.
const names: string[] = ["Jane Doe", "John Doe"];
const scores: number[] = [98, 72, 85];
const flags: boolean[] = [true, false, true];
Read string[] out loud as “an array of strings”. The brackets mean “many of these”. Once you’ve declared names as string[], TypeScript will guard it for you. Try to push the wrong type in, and it stops you before the code ever runs:
names.push("Sam Doe"); // fine, a string
names.push(42); // Error: number is not assignable to string
That error is the whole point. In plain JavaScript that 42 would slip in quietly and only blow up later, maybe in production, maybe inside a function that expected text. TypeScript catches it the moment you type it.
Arrays have no fixed length. You can start empty and add items, or remove them, and the type stays the same.
const queue: string[] = [];
queue.push("first");
queue.push("second");
queue.pop();
Let TypeScript infer when it can
If you write const scores = [98, 72, 85], TypeScript already figures out the type is number[] on its own. You only need to write the annotation explicitly when the array starts empty, or when you want to be stricter than what TypeScript would guess.
The other way to write it: Array<number>
You’ll run into a second style in real codebases. Instead of number[], some people write Array<number>. These two are exactly the same type — just two spellings of the same idea.
const a: number[] = [1, 2, 3];
const b: Array<number> = [1, 2, 3];
// a and b have identical types
The angle-bracket form is called generic syntax. You can think of Array<...> as a template, and the thing inside the brackets fills in the blank: “an array of what?” — “an array of number”.
So which should you use? It mostly comes down to readability and team convention. The number[] shorthand is shorter and reads naturally for simple types. The Array<...> form can be clearer when the inner type is long or itself complex, because the brackets group everything visibly.
// these get noisy with the shorthand
let pairs: [string, number][];
let pairsAlt: Array<[string, number]>; // arguably easier to scan
Pick one style and stay consistent across your project. Neither is “more correct”.
Tuples: when position has meaning
Now for the more interesting tool. Sometimes a list isn’t really “many of the same thing” — it’s a small, fixed set of values where each slot means something specific.
Think of a coordinate on a map: it’s always exactly two numbers, the first is the x-position and the second is the y-position. They’re both numbers, but they are not interchangeable. That’s a tuple: a fixed number of items, where each position has its own type and its own role.
You write a tuple with square brackets, but you list out each position’s type one by one:
let point: [number, number] = [10, 20];
let user: [string, number] = ["Jane Doe", 29]; // name, then age
Notice the difference from an array. number[] means “any amount of numbers”. [number, number] means “exactly two numbers, no more, no fewer”. TypeScript enforces both the length and the type at every position:
let point: [number, number] = [10, 20];
point = [10, 20, 30]; // Error: too many elements
point = ["10", 20]; // Error: position 0 must be a number
Tuples can mix types freely, which is exactly where they shine over a plain array:
// [is logged in, username, login count]
let session: [boolean, string, number] = [true, "John Doe", 3];
Reading values out of a tuple
You pull values out of a tuple the same way you would an array — by index, or with destructuring. Because TypeScript knows the type of each position, it gives you the right type for each one automatically.
let user: [string, number] = ["Jane Doe", 29];
const name = user[0]; // typed as string
const age = user[1]; // typed as number
// or, more readable, with destructuring:
const [fullName, years] = user;
This is far nicer than a plain (string | number)[] array, where every item could be either type and TypeScript can’t tell which is which.
When is a tuple the right tool?
A tuple is the right choice when you have a small, fixed group of related values whose order carries meaning. Some honest, everyday examples:
- A 2D coordinate:
[number, number]. - An RGB color:
[number, number, number]. - A “result or error” pair returned from a function:
[Error | null, Data | null]. - A key/value entry:
[string, number].
The classic place you already meet tuples — often without realizing it — is React’s useState, which hands back exactly two things in a fixed order: the current value, and a function to update it.
// useState returns a tuple: [value, setter]
const [count, setCount] = useState(0);
That said, tuples have a real limit: positions are identified by order, not by name. If a reader doesn’t know that index 0 is the name and index 1 is the age, the data is a guessing game. So here’s the practical rule:
Reach for an object when there are more than two or three fields
A tuple like [string, number, boolean, string] is hard to read because the meaning of each slot is invisible. Once you have several fields, an object with named keys ({ name, age, isActive }) is almost always clearer and easier to maintain. Tuples are best kept short.
Here’s the same data, both ways, so you can feel the difference:
// tuple — compact, but you must remember the order
const userTuple: [string, number, boolean] = ["Jane Doe", 29, true];
// object — longer, but self-documenting
const userObject = {
name: "Jane Doe",
age: 29,
isActive: true,
};
readonly: locking a list so nothing changes it
Both arrays and tuples can be made read-only. A read-only collection can be read and looped over freely, but it cannot be changed — no pushing, no popping, no reassigning a slot. TypeScript turns any attempt to mutate it into an error.
There are two ways to write it. For arrays, put the readonly keyword in front:
const colors: readonly string[] = ["red", "green", "blue"];
colors.push("yellow"); // Error: push does not exist on a readonly array
colors[0] = "black"; // Error: cannot assign to a readonly index
For tuples, you use the same readonly keyword:
const origin: readonly [number, number] = [0, 0];
origin[0] = 5; // Error: cannot change a readonly tuple
Why bother? Because mutation is one of the sneakiest sources of bugs. When you pass an array into a function, that function could quietly modify it, and the original changes underneath you. Marking the parameter readonly is a promise — to yourself and to your teammates — that the function will only look at the data, never alter it.
// this function cannot accidentally mutate the caller's array
function total(values: readonly number[]): number {
let sum = 0;
for (const v of values) sum += v;
return sum;
}
readonly is a compile-time guarantee
The readonly protection exists only while TypeScript checks your code. At runtime the value is still an ordinary JavaScript array, so it carries zero performance cost. It’s a guardrail for the people writing the code, not a lock the JavaScript engine enforces. That’s exactly why it’s free to use generously.
Array vs tuple at a glance
Here’s the whole picture side by side, so you can decide quickly next time:
| Question | Array (number[]) |
Tuple ([number, string]) |
|---|---|---|
| How many items? | Any amount, grows and shrinks | Fixed, decided up front |
| Item types | All the same type | Each position its own type |
| What does position mean? | Nothing special | Each slot has a fixed role |
| Best for | Collections of like things | Small fixed groups, ordered pairs |
| Read-only version | readonly number[] |
readonly [number, string] |
A simple gut check: if you’d be comfortable looping over every item with the same code, you want an array. If each spot means something different and the count never changes, you want a tuple.
"a list of users" -> array User[]
"a screen coordinate" -> tuple [number, number]
"all the order totals" -> array number[]
"a value-and-its-setter" -> tuple [State, Setter]
Recap
Arrays and tuples are the two ways TypeScript lets you describe a list, and knowing when to use each one will sharpen your types right away. Here’s what to carry forward:
- An array (
string[]orArray<string>— same thing) is a flexible list where every item is the same type and the length can change. Use it for collections of like things. - A tuple (
[string, number]) has a fixed length where each position has its own type and its own meaning. Use it for small, ordered groups like coordinates or a value/setter pair. - Keep tuples short. Once you pass two or three fields, an object with named keys is clearer.
- Add
readonlyto either one when a list should never be changed after it’s created. It costs nothing at runtime and prevents a whole class of mutation bugs.
Start by reaching for plain arrays — they cover most lists you’ll ever write. Save tuples for the moments when order itself carries meaning, and add readonly whenever you want a firm promise that the data stays put. Those three habits alone will make your types both safer and easier to read.