Announcing 5 new Flow tuple type features
Tuples are a lighter weight alternative to objects when you want to group data, by using positions rather than property names to distinguish elements. However, tuple types have lacked many useful features object types provide - until now!
- Labeled tuple elements:
type T = [foo: number, bar: string]
- Variance (read-only/write-only) annotations for tuple elements:
type T = [+foo: number, -bar: string]
- Optional tuple elements:
type T = [foo?: number, bar?: string]
- Tuple spread
type T = [number, string, ...OtherTuple]
- Refinement on tuple length
Read the full docs.
Labeled tuple elements
You can now add a label to tuple elements. Labels do not affect the type of the tuple element, but are useful in self-documenting the purpose of the tuple elements, especially when multiple elements have the same type.
type Range = [x: number, y: number];
Labels are also necessary to add variance annotations or optionality modifiers to elements.
Variance annotations on tuple elements and read-only tuples
You can add variance annotations (to denote read-only/write-only) on labeled tuple elements, just like on object properties:
type T = [+foo: number, -bar: string];
This allows you to mark elements as read-only or write-only. For example:
function f(readOnlyTuple: [+foo: number, +bar: string]) {
const n: number = readOnlyTuple[0]; // OK to read
readOnlyTuple[1] = 1; // ERROR! Cannot write
}
You can also use the $ReadOnly
on tuple types as a shorthand for marking each property as read-only:
type 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.
type T = [foo: number, bar?: string];
([1, "s"]: T); // OK: has all elements
([1]: 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:
type T = [foo?: number, bar?: number | void];
declare const x: T;
x[0] = undefined; // ERROR
([undefined]: T); // ERROR
x[1] = undefined; // OK: we've added `| void` to the element type
You can also use the Partial
and Required
utility types to make all elements optional or required respectively:
type AllRequired = [number, string];
([]: Partial<AllRequired>); // OK: like `[a?: number, b?: string]` now
type AllOptional = [a?: number, b?: string];
([]: Required<AllOptional>); // ERROR: like `[a: number, b: string]` now
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 type spread
You can spread a tuple type into another tuple type to make a longer tuple type:
type A = [number, string];
type T = [...A, boolean]; // Same as `[number, string, boolean]`
([1, "s", true]: T); // OK
Tuple spreads preserve variance and optionality. You cannot spread arrays into tuples, only other tuples.
Refinement on length
You can refine a union of tuples by their length:
type Union = [number, string] | [boolean];
function f(x: Union) {
if (x.length === 2) {
// `x` is `[number, string]`
const n: number = x[0]; // OK
const s: string = x[1]; // OK
} else {
// `x` is `[boolean]`
const b: boolean = x[0];
}
}
Technically this is not a newly added feature, but one not previously announced.
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.212.0prettier
: 3babel
withbabel-plugin-syntax-hermes-parser
. See our Babel guide for setup instructions.eslint
withhermes-eslint
. See our ESLint guide for setup instructions.
Bonus: declare const
& declare let
Flow now supports declare const
and declare let
(in addition to the existing declare var
). These allow you to declare a variable with a type, but without an implementation. They are useful for writing library definitions or creating reproductions of issues in Try Flow, while using the modern const
and let
instead of the legacy var
. Both declare const
and declare let
are block-scoped, like their non-declare
equivalents.
if (cond) {
declare const foo: number;
}
foo; // ERROR: cannot resolve `foo` (as it's block-scoped)