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, push
ing 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]
Inexact tuples
Inexact tuple types work like inexact objects: they allow for unknown members at the end of the tuple.
1[] as [...]; // OK2[1] as [...]; // OK3[1] as [number, ...]; // OK
All tuples are subtypes of the inexact empty tuple [...]
.
If you spread an inexact tuple, the result is also inexact. You cannot define elements after the spread of an inexact tuple, since we wouldn't know at what index they should be.
1declare const x: [1, ...];2const y = [0, ...x];3y as [0, 1]; // ERROR: it's inexact 4y as [0, 1, ...]; // OK5
6[...x, 2]; // ERROR: can't have element after inexact spread
3:1-3:1: Cannot cast `y` to tuple type because array literal [1] has 2 or more elements (inexact tuple) but tuple type [2] has 2 elements. [invalid-tuple-arity]6:8-6:8: Cannot have element after spread of inexact tuple. [element-after-inexact-tuple-spread]
Inexact tuples allow you to require that a generic is a tuple, e.g.
1function mapTupleArray<T: [...], R>(2 tuples: Array<T>, // An array of tuples3 f: (...T) => R, // Function args match the tuple's types4): Array<R> {5 return tuples.map(args => f(...args));6}7mapTupleArray(8 [[1, 'hi'], [3, 'bye']],9 (x: number, y: string) => y.length === x,10); // OK11
12declare const arrays: Array<Array<number>>;13mapTupleArray(arrays, (x: number, y: number) => x + y); // ERROR: array is not a tuple
13:1-13:13: Cannot call `mapTupleArray` because array type [1] has an unknown number of elements, so is incompatible with tuple type [2] in type argument `T`. [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
andflow-parser
: 0.212prettier
: 3babel
withbabel-plugin-syntax-hermes-parser
(v0.15). See our Babel guide for setup instructions.eslint
withhermes-eslint
(v0.15). See our ESLint guide for setup instructions.
To use inexact tuples, upgrade to:
flow
andflow-parser
: 0.243prettier
: 3.3babel
withbabel-plugin-syntax-hermes-parser
(v0.23). See our Babel guide for setup instructions.eslint
withhermes-eslint
(v0.23). See our ESLint guide for setup instructions.