Announcing 5 new Flow tuple type features

George Zahariev
Flow
Published in
3 min readAug 17, 2023

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!

  1. Labeled tuple elements: type T = [foo: number, bar: string]
  2. Variance (read-only/write-only) annotations for tuple elements: type T = [+foo: number, -bar: string]
  3. Optional tuple elements: type T = [foo?: number, bar?: string]
  4. Tuple spread type T = [number, string, ...OtherTuple]
  5. 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 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.

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)

--

--