Skip to main content

Tuples

Tuple types represent a fixed length list, where the elements can have different types. This is in contrast to array types, which have an unknown length and all elements have the same type.

Tuple Basics

JavaScript array literal values can be used to create both tuple and array types:

1const arr: Array<number> = [1, 2, 3]; // As an array type2const tup: [number, number, number] = [1, 2, 3]; // As a tuple type

In Flow you can create tuple types using the [type1, type2, type3] syntax:

1const tuple1: [number] = [1];2const tuple2: [number, boolean] = [1, true];3const tuple3: [number, boolean, string] = [1, true, "three"];

When you get a value from a tuple at a specific index, it will return the type at that index:

1const tuple: [number, boolean, string] = [1, true, "three"];2
3const num: number = tuple[0]; // Works!4const bool: boolean = tuple[1]; // Works!5const str: string  = tuple[2]; // Works!

Trying to access an index that does not exist results in an index-out-of-bounds error:

1const tuple: [number, boolean, string] = [1, true, "three"];2
3const none = tuple[3]; // Error!
3:14-3:21: Cannot get `tuple[3]` because tuple type [1] only has 3 elements, so index 3 is out of bounds. [invalid-tuple-index]

If Flow doesn't know which index you are trying to access it will return all possible types:

1const tuple: [number, boolean, string] = [1, true, "three"];2
3function getItem(n: number) {4  const val: number | boolean | string = tuple[n];5  // ...6}

When setting a new value inside a tuple, the new value must match the type at that index:

1const tuple: [number, boolean, string] = [1, true, "three"];2
3tuple[0] = 2;     // Works!4tuple[1] = false; // Works!5tuple[2] = "foo"; // Works!6
7tuple[0] = "bar"; // Error!
8tuple[1] = 42; // Error!
9tuple[2] = false; // Error!
7:12-7:16: Cannot assign `"bar"` to `tuple[0]` because string [1] is incompatible with number [2]. [incompatible-type]
8:12-8:13: Cannot assign `42` to `tuple[1]` because number [1] is incompatible with boolean [2]. [incompatible-type]
9:12-9:16: Cannot assign `false` to `tuple[2]` because boolean [1] is incompatible with string [2]. [incompatible-type]

Strictly enforced tuple length (arity)

The length of the tuple is known as the "arity". The length of a tuple is strictly enforced in Flow.

This means that a shorter tuple can't be used in place of a longer one:

1const tuple1: [number, boolean] = [1, true];2
3const tuple2: [number, boolean, void] = tuple1; // Error!
3:41-3:46: Cannot assign `tuple1` to `tuple2` because tuple type [1] has 2 elements but tuple type [2] has 3 elements. [invalid-tuple-arity]

Also, a longer tuple can't be used in place of a shorter one:

1const tuple1: [number, boolean, void] = [1, true, undefined];2
3const tuple2: [number, boolean] = tuple1; // Error!
3:35-3:40: Cannot assign `tuple1` to `tuple2` because tuple type [1] has 3 elements but tuple type [2] has 2 elements. [invalid-tuple-arity]

Optional elements make the arity into a range.

Tuples don't match array types

Since Flow does not know the length of an array, an Array<T> type cannot be passed into a tuple:

1const array: Array<number> = [1, 2];2
3const tuple: [number, number] = array; // Error!
3:33-3:37: Cannot assign `array` to `tuple` because array type [1] has an unknown number of elements, so is incompatible with tuple type [2]. [invalid-tuple-arity]

Also a tuple type cannot be passed into to an Array<T> type, since then you could mutate the tuple in an unsafe way (for example, pushing a third item onto it):

1const tuple: [number, number] = [1, 2];2
3const array: Array<number> = tuple; // Error!
3:30-3:34: Cannot assign `tuple` to `array` because tuple type [1] is incompatible with array type [2]. [incompatible-type]

However, you can pass it to a $ReadOnlyArray type, since mutation is disallowed:

1const tuple: [number, number] = [1, 2];2
3const array: $ReadOnlyArray<number> = tuple; // Works!

Cannot use mutating array methods on tuples

You cannot use Array.prototype methods that mutate the tuple, only ones that do not:

1const tuple: [number, number] = [1, 2];2tuple.join(', '); // Works!3
4tuple.push(3); // Error!
4:7-4:10: Cannot call `tuple.push` because property `push` is missing in `$ReadOnlyArray` [1]. [prop-missing]

Length refinement

You can refine a union of tuples by their length:

1type Union = [number, string] | [boolean];2function f(x: Union) {3  if (x.length === 2) {4    // `x` is `[number, string]`5    const n: number = x[0]; // OK6    const s: string = x[1]; // OK7  } else {8    // `x` is `[boolean]`9    const b: boolean = x[0];10  }11}

Tuple element labels

NOTE: This and the following sections require your tooling to be updated as described in the "Adoption" section at the end of this page.

You can add a label to tuple elements. This label does not affect the type of the tuple element, but is useful in self-documenting the purpose of the tuple elements, especially when multiple elements have the same type.

1type Range = [x: number, y: number];

The label is also necessary to add a variance annotation or optionality modifier to an element (as without the label we would have parsing ambiguities).

Variance annotations and read-only tuples

You can add variance annotations (to denote read-only/write-only) on labeled tuple elements, just like on object properties:

1type T = [+foo: number, -bar: string];

This allows you to mark elements as read-only or write-only. For example:

1function f(readOnlyTuple: [+foo: number, +bar: string]) {2  const n: number = readOnlyTuple[0]; // OK to read3  readOnlyTuple[1] = 1; // ERROR! Cannot write
4}
3:3-3:18: Cannot assign `1` to `readOnlyTuple[1]` because tuple element at index `1` [1] labeled `bar` is not writable. [cannot-write]
3:22-3:22: Cannot assign `1` to `readOnlyTuple[1]` because number [1] is incompatible with string [2]. [incompatible-type]

You can also use the $ReadOnly on tuple types as a shorthand for marking each property as read-only:

1type T = $ReadOnly<[number, string]>; // Same as `[+a: number, +b: string]`

Optional tuple elements

You can mark tuple elements as optional with ? after an element’s label. This allows you to omit the optional elements. Optional elements must be at the end of the tuple type, after all required elements.

1type T = [foo: number, bar?: string];2[1, "s"] as T; // OK: has all elements3[1] as T; // OK: skipping optional element

You cannot write undefined to the optional element - add | void to the element type if you want to do so:

1type T = [foo?: number, bar?: number | void];2declare const x: T;3x[0] = undefined; // ERROR
4[undefined] as T; // ERROR
5 6x[1] = undefined; // OK: we've added `| void` to the element type
3:8-3:16: Cannot assign `undefined` to `x[0]` because you cannot assign undefined [1] to optional tuple element [2] (to do so, add `| void` to the tuple element type). [incompatible-type]
4:2-4:10: Cannot cast array literal to `T` because you cannot assign undefined [1] to optional tuple element [2] (to do so, add `| void` to the tuple element type) in index 0. [incompatible-cast]

You can also use the Partial and Required utility types to make all elements optional or required respectively:

1type AllRequired = [number, string];2[] as Partial<AllRequired>; // OK: like `[a?: number, b?: string]` now3
4type AllOptional = [a?: number, b?: string];5[] as Required<AllOptional>; // ERROR: like `[a: number, b: string]` now
5:1-5:2: Cannot cast array literal to required of `AllOptional` because empty array literal [1] has 0 elements but `AllOptional` [2] has 2 elements. [invalid-tuple-arity]

Tuples with optional elements have an arity (length) that is a range rather than a single number. For example, [number, b?: string] has an length of 1-2.

Tuple spread

You can spread a tuple type into another tuple type to make a longer tuple type:

1type A = [number, string];2type T = [...A, boolean]; // Same as `[number, string, boolean]`3[1, "s", true] as T; // OK

Tuple spreads preserve labels, variance, and optionality. You cannot spread arrays into tuples, only other tuples.

At the value level, if you spread a tuple with optional elements into an array literal, then you cannot have anything after that spread and retain the tuple view of the array value. That is because a tuple with optional elements has a length that's a range, so we don't know at what index any subsequent values would be at. You can still type this value as the appropriate Array<T> type - only the tuple view of the value is affected.

1const a: [foo?: 1] = [];2const b = [0, ...a, 2]; // At runtime this is `[0, 2]`3b as [0, 1 | void, 2]; // ERROR
4b as Array<number | void>; // OK5 6const c: [0, foo?: 1] = [0];7const d: [bar?: 2] = [2];8const e = [...c, ...d]; // At runtime this is `[0, 2]`9e as [0, foo?: 1, bar?: 2]; // ERROR
10e as Array<number | void>; // OK
3:1-3:1: Cannot cast `b` to tuple type because array literal [1] has an unknown number of elements, so is incompatible with tuple type [2]. [invalid-tuple-arity]
9:1-9:1: Cannot cast `e` to tuple type because array literal [1] has an unknown number of elements, so is incompatible with tuple type [2]. [invalid-tuple-arity]

Adoption

To use labeled tuple elements (including optional elements and variance annotations on elements) and tuple spread elements, you need to upgrade your infrastructure so that it supports the syntax:

  • flow and flow-parser: 0.212.0
  • prettier: 3
  • babel with babel-plugin-syntax-hermes-parser. See our Babel guide for setup instructions.
  • eslint with hermes-eslint. See our ESLint guide for setup instructions.