Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flow doesn’t let me pass `Array<A>` to `Array<A | B>` (array of subtypes to array of supertypes)

I have a value of type Array<A> (an array of subtypes). Flow doesn’t let me pass it to a place that expects Array<A | B> (an array of supertypes), even though it obviously works.

For example, I can’t assign a value with type Array<'left' | 'right'> to a variable whose type is Array<string>:

const directions: Array<'left' | 'right'> = ['right', 'left'];
const messages: Array<string> = directions; // error

This error is raised:

2: const messages: Array<string> = directions; // error
                                   ^ Cannot assign `directions` to `messages` because in array element: Either string [1] is incompatible with string literal `left` [2]. Or string [1] is incompatible with string literal `right` [3].
    References:
    2: const messages: Array<string> = directions; // error
                             ^ [1]
    1: const directions: Array<'left' | 'right'> = ['right', 'left'];
                               ^ [2]
    1: const directions: Array<'left' | 'right'> = ['right', 'left'];
                                        ^ [3]

Try Flow demo

Similarly, I can’t pass an Array<ANode> to a function that takes Array<Node>, even though Node is ANode | BNode:

type ANode = {type: 'a', value: string};
type BNode = {type: 'b', count: number};

type Node = ANode | BNode;

function getFirstNodeType(nodes: Array<Node>): string {
    return nodes[0].type;
}

// works
const nodesSupertype: Array<Node> = [{type: 'a', value: 'foo'}];
getFirstNodeType(nodesSupertype);

// error
const nodesSubtype: Array<ANode> = [{type: 'a', value: 'foo'}];
getFirstNodeType(nodesSubtype); // error
16: getFirstNodeType(nodesSubtype); // error
                    ^ Cannot call `getFirstNodeType` with `nodesSubtype` bound to `nodes` because property `value` is missing in `BNode` [1] but exists in `ANode` [2] in array element.
References:
6: function getFirstNodeType(nodes: Array<Node>): string {
                                            ^ [1]
15: const nodesSubtype: Array<ANode> = [{type: 'a', value: 'foo'}];
                                ^ [2]

16: getFirstNodeType(nodesSubtype); // error
                    ^ Cannot call `getFirstNodeType` with `nodesSubtype` bound to `nodes` because string literal `a` [1] is incompatible with string literal `b` [2] in property `type` of array element.
References:
1: type ANode = {type: 'a', value: string};
                        ^ [1]
2: type BNode = {type: 'b', count: number};
                        ^ [2]

Try Flow demo

like image 854
Rory O'Kane Avatar asked Sep 21 '25 05:09

Rory O'Kane


1 Answers

Flow raises an error because it thinks you might mutate the array:

// given the definitions of `Node`, `ANode`, and `BNode` from the question’s second example

function getFirstNodeType(nodes: Array<Node>): string {
    const bNode: BNode = {type: 'b', count: 0}
    // Mutate the parameter `nodes`. Adding a `BNode` is valid according its type.
    nodes.push(bNode);

    return nodes[0].type;
}

const nodesSubtype: Array<ANode> = [{type: 'a', value: 'foo'}];
getFirstNodeType(nodesSubtype); // error

Try Flow demo

After running the above code, nodesSubtype would contain a BNode even though it is declared as an array of ANode, violating its type.

There are two solutions to convince Flow that you won’t mutate the array. The clearest one is to replace Array with the Flow utility type $ReadOnlyArray.

function getFirstNodeType(nodes: $ReadOnlyArray<Node>): string { // use $ReadOnlyArray
    return nodes[0].type;
}

const nodesSubtype: Array<ANode> = [{type: 'a', value: 'foo'}];
getFirstNodeType(nodesSubtype); // no error

Try Flow demo

You only have to make that replacement in the function parameter’s type (e.g. nodes), but if you liked you could use $ReadOnlyArray everywhere it applies (e.g. for nodesSubtype).

$ReadOnlyArray is the most type-safe solution, but you may not want to have to change all your existing functions to use it. In that case, your alternative is to cast the array type through any instead. Yes, you could have done that from the beginning, but at least now you know why it’s safe to do this cast. You can even leave a comment explaining why this cast exists, which might help you catch assumptions no longer being valid.

function getFirstNodeType(nodes: Array<Node>): string {
    return nodes[0].type;
}

const nodesSubtype: Array<ANode> = [{type: 'a', value: 'foo'}];
// casting `Array<ANode>` to `Array<Node>` is okay because we know that `getFirstNodeType` won’t actually modify the array
getFirstNodeType(((nodesSubtype: any): Array<Node>));

Try Flow demo

If you don’t care about documenting the problem, you can omit the comment and only cast to any:

getFirstNodeType((nodesSubtype: any));

Try Flow demo

Additional resources

  • $ReadOnlyArray became documented in August 2018, thanks to this pull request.
  • The older article “Flow’s best kept secret” describes a third possible workaround, involving wrapping the function’s array type in a generic type.
like image 145
Rory O'Kane Avatar answered Sep 22 '25 17:09

Rory O'Kane