Alright, let’s talk about tuples. You’ve met arrays, which are great for lists of things where everything is the same type. But what about when you need a fixed-length, ordered structure where each position has a specific and potentially different type? Enter the tuple.

Think of it as a formally defined couple or trio. It’s not “a list of stuff,” it’s “exactly two things: a string and a number, in that order.” This is incredibly useful for things like representing coordinates ([number, number]), key-value pairs ([string, any]), or returning multiple values from a function without the ceremony of creating a new object or class.

The Basic Syntax: It’s Just an Array, But Lying

Syntactically, a tuple type looks just like an array type, but you specify the type for each position.

// This is an array: all elements are numbers.
const scores: number[] = [1, 2, 3];

// This is a tuple: the first element is a string, the second is a number.
let coordinate: [string, number];
coordinate = ['x', 42]; // ✅ Perfect.
coordinate = [42, 'x']; // ❌ Nope. The order matters. TypeScript will yell at you.

The beauty here is in the type safety. You know that if you have a value of type [string, number], you can safely access element[0] as a string and element[1] as a number.

Beyond the Basics: Optional and Rest Elements

The designers didn’t stop at just fixed pairs. They knew we’d need a little flexibility, so they introduced optional elements. Because sometimes, the third wheel doesn’t always show up to the party.

// A tuple for a HTTP response: [statusCode, statusMessage, headers?]
let httpResponse: [number, string, string?];
httpResponse = [200, 'OK']; // ✅ Header is optional.
httpResponse = [200, 'OK', 'Content-Type: application/json']; // ✅ Also valid.

// You can also have rest elements, which are frankly just array types showing up late to the tuple party.
// This represents a tuple that must start with a string and a number, but can have any number of booleans after.
type NameAndValueAndFlags = [string, number, ...boolean[]];
const example: NameAndValueAndFlags = ['price', 9.99, true, false, true];

The rest element is how TypeScript represents its variadic tuple types, which is a fancy term for “tuples that can have a variable number of elements at the end.” It’s incredibly powerful for typing function arguments.

The Ridiculous Edge Case: push and pop

Here’s where we hit a design choice that is, frankly, a bit absurd. Tuples are supposed to be fixed-length. But because under the hood, they’re just JavaScript arrays, all the mutable array methods are still available. And TypeScript, in its infinite wisdom, allows you to use them without complaint.

let fixedTuple: [string, number] = ['hello', 42];

// This should be illegal, right? We're breaking the contract!
fixedTuple.push('world'); // ✅ TypeScript is... fine with this?!
console.log(fixedTuple); // Logs: ['hello', 42, 'world']
console.log(fixedTuple[2]); // ❌ But accessing index 2 is a type error!

Let’s be direct: this is weird. Your type says it’s a fixed-length two-item array, but you can still mutate it into something else. The type system cheerfully ignores the mutation, leaving you with a runtime value that doesn’t match the compile-time type. It’s the language’s way of saying, “I’ve done my job by defining the type, but I’m not your babysitter.” It’s a conscious choice for practicality—imagine the overhead of making every array method type-safe—but it means you must be disciplined. Don’t use push, pop, or splice on a tuple if you want to maintain its integrity. Treat it as a read-only structure after creation.

The Right Way: readonly Tuples

The best practice, and the way to avoid the aforementioned nonsense, is to make your tuples readonly. This locks the structure down completely and prevents those pesky mutable methods from being called.

function getCoordinate(): readonly [number, number] {
  return [10, 20];
}

const coord = getCoordinate();
coord[0] = 99; // ❌ Error! Cannot assign to '0' because it is a read-only property.
coord.push(30); // ❌ Error! Property 'push' does not exist on type 'readonly [number, number]'.

This is how you should be using tuples 99% of the time. You define a strict shape for data, and you want that shape to be reliable. readonly enforces that contract. It’s the difference between a handshake agreement and a legally binding document. Use it.