Typescript Tuple Types

How to best type tuples in Typescript 4.2 and later

A closer look at Typescript tuples

As a quick reminder, a tuple in its most basic definition is just a data structure that consists of multiple parts. In scope of using tuples in programming languages, such as Typescript, it’s also important to note that the data is most commonly ordered.

A simple example shows how tuples are defined in Typescript.

// This tuple is defined as a set
// of two numbers.
const scores: [number, number] = [1, 2];

// For comparison, this tuple consists
// of three elements, each of a different
// type. Not that the ordered nature of
// tuples in TS becomes very clear here.
const result: [string, number, boolean] = ["id", 101, false];

// And as a "nice-to-know", you can even
// provide lables for the tuple elements.
// This doesn't have any effect on the typesystem
// itself and only (may) improve documentation.
const output: [id: number, name: string] = [101, "Tom"];

Tuples with optional elements

As Typescript got better over time, so did its implementation of tuples. You don’t only have to define elements that have to be mandatory. You can now also type elements as optional. If you don’t know, Typescript uses the question mark as a general symbol to define elements as optional, which means they can be available at runtime, but don’t have to.

Another example demonstrates what I mean by that.

// Similar to our previous example, but in this
// case the the tuple's last element doesn't have
// to be provided (or can be undefined at runtime).
type Tuple = [id: number, name?: string];

const a: Tuple = [101];
const b: Tuple = [42, "Tom"];

Rest elements in Typescript tuples

With rest elements, you have a very powerful type at hand that marks all following elements in the tuple of a given type. So say you have a tuple with two elements and the second one defined as a rest element, you can then provide 2 + n elements at runtime to this tuple variable.

To narrow down the definition, until recently such an element was only allowed at the end of a tuple. This makes sense, as this allows you to provide any number of elements at runtime of the rest type, but would make things very complicated to distinguish then between the rest element and one more, other typed item.

// This example might be a tuple type
// for a CLI similar to Node.js. The first
// two elements are system-internal.
//
// Starting from the 3rd element, a user can
// provide as much arguments as desired, yet
// we can still cleanly handle it with TS. Nice!
let input: [number, boolean, ...string[]];

// Just to show that we really can provide any
// number of rest elements, including 0.
e = [0, false, "max-cache", "1024", "debug", "false"];
e = [0, false];
e = [0, false, "verbose"];

Leading or middle rest element in tuple types

Advancing the rest element for tuples, you can create even more sophisticated implementations since Typescript 4.2 was released. And I have to apologize here: just a few sentences early, I wrote how it’s mandatory to use a rest element only as the last one. This restriction isn't actually true anymore since Typescript 4.2, as you can now place rest elements almost anywhere in a tuple.

But with only a few restrictions, Typescript now provides a very nice syntax for advanced tuples. Rest elements can occur anywhere within a tuple as long as they conform to the following two rules

  • it’s not followed by an optional element
  • no other rest element follows the first one

Before talking too much theory, let’s see for an example.

// And here comes the fancy part: rest elements
// *not only* at the end of a tuple.
// 
// Note: this example is taken directly from the
// TS documentation. For more details, check out the
// links in the addendum.
let foo: [...string[], number];

foo = [123];
foo = ["hello", 123];
foo = ["hello!", "hello!", "hello!", 123];

let bar: [boolean, ...string[], boolean];

bar = [true, false];
bar = [true, "some text", false];
bar = [true, "some", "separated", "text", false];

// And here's an example that shows how the
// type system would catch your errors:
interface Clown { /*...*/ }
interface Joker { /*...*/ }

let StealersWheel: [...Clown[], "me", ...Joker[]];
//                                    ~~~~~~~~~~ Error!
// A rest element cannot follow another rest element.

let StringsAndMaybeBoolean: [...string[], boolean?];
//                                        ~~~~~~~~ Error!
// An optional element cannot follow a rest element.

The rest element of this article

Closing in on this compact guide about tuples in Typescript, we’ve taken a look at the basic implementation and then went through some more advanced examples to see how Typescript allows for a very flexible type system when it comes to tuples. I hope you enjoyed the article and if you’re curious to learn more, check out the suggested posts other ones down below.

Suggestions

Related

Addendum

Languages