Skip to main content

Arrays

Array types represent lists of unknown length, where all items have the same type. This is in contrast to tuple types, which have a fixed length and where each element can have a different type.

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

Array Type

The type Array<T> represents an array of items of type T. For example, an array of numbers would be Array<number>:

1const arr: Array<number> = [1, 2, 3];

You can put any type within Array<T>:

1const arr1: Array<boolean> = [true, false, true];2const arr2: Array<string> = ["A", "B", "C"];3const arr3: Array<mixed> = [1, true, "three"];

$ReadOnlyArray Type

You can use the type $ReadOnlyArray<T> instead of Array<T> to represent a read-only array which cannot be mutated. You can't write to a read-only array directly, and can't use methods which mutate the array like .push(), .unshift(), etc.

1const readonlyArray: $ReadOnlyArray<number> = [1, 2, 3]2
3const first = readonlyArray[0]; // OK to read4readonlyArray[1] = 20;          // Error!
5readonlyArray.push(4); // Error!
6readonlyArray.unshift(4); // Error!
4:1-4:16: Cannot assign `20` to `readonlyArray[1]` because read-only arrays cannot be written to. [cannot-write]
5:15-5:18: Cannot call `readonlyArray.push` because property `push` is missing in `$ReadOnlyArray` [1]. [prop-missing]
6:15-6:21: Cannot call `readonlyArray.unshift` because property `unshift` is missing in `$ReadOnlyArray` [1]. [prop-missing]

Note that an array of type $ReadOnlyArray<T> can still have mutable elements:

1const readonlyArray: $ReadOnlyArray<{x: number}> = [{x: 1}];2readonlyArray[0] = {x: 42}; // Error!
3readonlyArray[0].x = 42; // Works
2:1-2:16: Cannot assign object literal to `readonlyArray[0]` because read-only arrays cannot be written to. [cannot-write]

The main advantage to using $ReadOnlyArray instead of Array is that $ReadOnlyArray's type parameter is covariant while Array's type parameter is invariant. That means that $ReadOnlyArray<number> is a subtype of $ReadOnlyArray<number | string> while Array<number> is NOT a subtype of Array<number | string>. So it's often useful to use $ReadOnlyArray in type annotations for arrays of various types of elements. Take, for instance, the following scenario:

1const someOperation = (arr: Array<number | string>) => {2  // Here we could do `arr.push('a string')`3}4
5const array: Array<number> = [1];6someOperation(array) // Error!
6:15-6:19: Cannot call `someOperation` with `array` bound to `arr` because string [1] is incompatible with number [2] in array element. Arrays are invariantly typed. See https://flow.org/en/docs/faq/#why-cant-i-pass-an-arraystring-to-a-function-that-takes-an-arraystring-number. [incompatible-call]

Since the parameter arr of the someOperation function is typed as a mutable Array, pushing a string into it would be possible inside that scope, which would then break the type contract of the outside array variable. By annotating the parameter as $ReadOnlyArray instead in this case, Flow can be sure this won't happen and no errors will occur:

1const someOperation = (arr: $ReadOnlyArray<number | string>) => {2  // Nothing can be added to `arr`3}4
5const array: Array<number> = [1];6someOperation(array); // Works

$ReadOnlyArray<mixed> represents the supertype of all arrays and all tuples:

1const tup: [number, string] = [1, 'hi'];2const arr: Array<number> = [1, 2];3
4function f(xs: $ReadOnlyArray<mixed>) { /* ... */ }5
6f(tup); // Works7f(arr); // Works

Empty Array Literals

Empty array literals ([]) are handled specially in Flow when it comes to their annotation requirements. What makes them special is that they do not contain enough information to determine their type, and at the same time they are too common to always require type annotations in their immediate context.

So, to type empty arrays Flow follows these rules:

Contextual Inference

First, if contextual information exists, we'll use it to determine the array element type:

1function takesStringArray(x: Array<string>): void {}2
3const arr1: Array<string> = [];4takesStringArray([]);

In both cases, the [] will be typed as an Array<string>.

Note that for the contextual information to work, the type needs to be available right at the definition of the array. This means that the last two lines in the following code will error:

1function takesStringArray(x: Array<string>): void {}2
3const arr2 = [];
4takesStringArray(arr2);
3:7-3:10: Cannot determine type of empty array literal. Please provide an annotation. [missing-empty-array-annot]
4:18-4:21: Cannot call `takesStringArray` with `arr2` bound to `x` because string [1] is incompatible with unknown element of empty array [2] in array element. Arrays are invariantly typed. See https://flow.org/en/docs/faq/#why-cant-i-pass-an-arraystring-to-a-function-that-takes-an-arraystring-number. [incompatible-call]

The second error is due to arr2 being inferred as Array<empty> which leads to another error at the call to takesStringArray.

Initializer Inference

Flow allows another way to determine the types of empty arrays when they are immediately assigned to a variable:

const arr3 = [];

The way it does this is reminiscent of the typing of variables without initializers: Flow tries to choose the "first" assignment or assignments to the variable to define its type. In the case of empty arrays, an assignment is either

  • an indexed write statement a[i] = e;, or
  • an array push call a.push(e).

In either case the type of e is used as the type of the array element.

Here are some examples:

Straight-line Code

Once the first assignemnt has been found, the type of the array element is pinned to that of the assigned expression. Subsequent writes to array with elements of a different type are errors:

1const arr3 = [];2arr3.push(42); // arr3 is inferred as Array<number>3arr3.push("abc"); // Error!
3:11-3:15: Cannot call `arr3.push` because string [1] is incompatible with number [2] in array element. [incompatible-call]

Conditional Code

If the array is assigned in sibling branches of conditional statements, the type of the array element is pinned to the union of the assigned types:

1declare const cond: boolean;2
3const arr4 = [];4if (cond) {5  arr4[0] = 42;6} else {7  arr4.push("abc");8}9// arr4 is inferred as Array<number | string>10arr4.push("def"); // Works11arr4[0] = true; // Error!
11:11-11:14: Cannot assign `true` to `arr4[0]` because: [incompatible-type] Either boolean [1] is incompatible with number [2]. Or boolean [1] is incompatible with string [3].

Nearer Scope Wins

Shallow scope of assignment is prefered when there are multiple scopes where assignments happen:

1const arr5 = [];2function f() {3  arr5.push(42); // Error!
4}5f();6arr5.push("abc"); // This assignment wins. arr5 is inferred as Array<string>7arr5.push(1); // Error!
3:13-3:14: Cannot call `arr5.push` because number [1] is incompatible with string [2] in array element. [incompatible-call]
7:11-7:11: Cannot call `arr5.push` because number [1] is incompatible with string [2] in array element. [incompatible-call]

Array access is unsafe

When you retrieve an element from an array there is always a possibility that it is undefined. You could have either accessed an index which is out of the bounds of the array, or the element could not exist because it is a "sparse array".

For example, you could be accessing an element that is out of the bounds of the array:

1const array: Array<number> = [0, 1, 2];2const value: number = array[3]; // Works3                         // ^ undefined

Or you could be accessing an element that does not exist if it is a "sparse array":

1const array: Array<number> = [];2
3array[0] = 0;4array[2] = 2;5
6const value: number = array[1]; // Works7                         // ^ undefined

In order to make this safe, Flow would have to mark every single array access as "possibly undefined".

Flow does not do this because it would be extremely inconvenient to use. You would be forced to refine the type of every value you get when accessing an array.

1const array: Array<number> = [0, 1, 2];2const value: number | void = array[1];3
4if (value !== undefined) {5  // number6}

Discouraged Array Type Shorthand Syntax

There is an alternative to the Array<T> syntax: T[]. This syntax is discouraged and may be deprecated in the future.

1const arr: number[] = [0, 1, 2, 3];

Just note that ?Type[] is the equivalent of ?Array<T> and not Array<?T>.

1const arr1: ?number[] = null;   // Works2const arr2: ?number[] = [1, 2]; // Works3const arr3: ?number[] = [null]; // Error!
3:26-3:29: Cannot assign array literal to `arr3` because null [1] is incompatible with number [2] in array element. [incompatible-type]

If you want to make it Array<?T> you can use parenthesis like: (?Type)[]

1const arr1: (?number)[] = null;   // Error!
2const arr2: (?number)[] = [1, 2]; // Works3const arr3: (?number)[] = [null]; // Works
1:27-1:30: Cannot assign `null` to `arr1` because null [1] is incompatible with array type [2]. [incompatible-type]