Flow: Can’t assign values to `mixed` when it’s within an Array or Object type

Created on 7 Jul 2020  Â·  1Comment  Â·  Source: facebook/flow

Flow version: v0.128.0

Expected behavior

According to the documentation page Mixed Types, anything can be assigned to mixed:

mixed will accept any type of value. Strings, numbers, objects, functions– anything will work.

That documentation page doesn’t say it should work any different when mixed is inside an array or object. Therefore, the following code, which uses an Array<mixed> type, should pass Flow type checking:

const arrayOfNumbers: Array<number> = [1, 2, 3];
const arrayOfMixed: Array<mixed> = arrayOfNumbers;

Additionally, all of the example code in the “Actual behavior” section should pass Flow type checking.

I know that covariance and contravariance can sometimes cause similar confusion, but I don’t see why it would change my expectations here.

Actual behavior

Flow raises type errors in four of the six paragraphs of code below (demo on Try Flow):

~~~ts
// @flow

// mixed works when it’s a top-level value
const num: number = 3;
const mix: mixed = num;

// mixed FAILS inside array types
const arrayOfNumbers: Array = [1, 2, 3];
const arrayOfMixed: Array = arrayOfNumbers;

// mixed FAILS inside object types
const personWithNumberAge: { age: number } = { age: 55 };
const personWithMixedAge: { age: mixed } = personWithNumberAge;

// mixed FAILS inside indexed object types
const stringsToNumbers: { [string]: number } = { "a": 1, "b": 2 };
const stringsToMixed: { [string]: mixed } = stringsToNumbers;

// mixed FAILS inside indexed object types as function parameters (my use-case)
function withStringsToMixed(obj: { [string]: mixed }): Array {
return Object.keys(obj);
}
withStringsToMixed(stringsToNumbers);

// generics work inside indexed object types
function withStringsToGeneric(obj: { [string]: T }): Array {
return Object.keys(obj);
}
withStringsToGeneric(stringsToNumbers);
~~~

The type errors are of this form:

Cannot assign somethingContainingNumbers to somethingContainingMixed because number [1] is incompatible with mixed [2] in [some container]. [incompatible-type]

All four errors that Flow raises for the above example code:

~
9: const arrayOfMixed: Array = arrayOfNumbers;
^ Cannot assign arrayOfNumbers to arrayOfMixed because number [1] is incompatible with mixed [2] in array element. [incompatible-type]
References:
8: const arrayOfNumbers: Array = [1, 2, 3];
^ [1]
9: const arrayOfMixed: Array = arrayOfNumbers;
^ [2]
13: const personWithMixedAge: { age: mixed } = personWithNumberAge;
^ Cannot assign personWithNumberAge to personWithMixedAge because number [1] is incompatible with mixed [2] in property age. [incompatible-type]
References:
12: const personWithNumberAge: { age: number } = { age: 55 };
^ [1]
13: const personWithMixedAge: { age: mixed } = personWithNumberAge;
^ [2]
17: const stringsToMixed: { [string]: mixed } = stringsToNumbers;
^ Cannot assign stringsToNumbers to stringsToMixed because number [1] is incompatible with mixed [2] in the indexer property. [incompatible-type]
References:
16: const stringsToNumbers: { [string]: number } = { "a": 1, "b": 2 };
^ [1]
17: const stringsToMixed: { [string]: mixed } = stringsToNumbers;
^ [2]
23: withStringsToMixed(stringsToNumbers);
^ Cannot call withStringsToMixed with stringsToNumbers bound to obj because number [1] is incompatible with mixed [2] in the indexer property. [incompatible-call]
References:
16: const stringsToNumbers: { [string]: number } = { "a": 1, "b": 2 };
^ [1]
20: function withStringsToMixed(obj: { [string]: mixed }): Array {
^ [2]
~

The last paragraph of my code example, “generics work inside indexed object types”, shows that this issue can be worked around by adding an unused generic parameter to the function. This workaround is only possible if the mixed-containing type is a parameter to the function, not if it’s a local variable.

For comparison, when I try writing equivalent code in TypeScript by changing mixed to unknown, TypeScript raises no errors. I know that Flow and TypeScript are not always directly comparable, though, since their type systems differ in a few ways.

not a bug

Most helpful comment

I know that covariance and contravariance can sometimes cause similar confusion, but I don’t see why it would change my expectations here.

Here is why:

const arrayOfNumbers: Array<number> = [1, 2, 3];
const arrayOfMixed: Array<mixed> = arrayOfNumbers;
arrayOfMixed[0] = 'foo'; // allowed for `Array<mixed>` but violates `Array<number>`

Because arrayOfMixed is allowed to be mutated and because it is referentially the same array as arrayOfNumbers it is possible that line 3 could occur and is thus an error to allow line 2. There are a couple ways around this.

// use spread to create a new reference which can be mutated without affecting the `arrayOfNumbers`
const arrayOfMixed: Array<mixed> = [...arrayOfNumbers];

// use a read-only array to prevent mutation
const arrayOfMixed: $ReadOnlyArray<mixed> = arrayOfNumbers;

In your follow up examples:

// mixed works when it’s a top-level value
const num: number = 3;
const mix: mixed = num;
// This is allowed because they are constants and can never be reassigned or mutated.
// mixed FAILS inside object types
const personWithNumberAge: { age: number } = { age: 55 };
const personWithMixedAge: { age: mixed } = personWithNumberAge;
// This fails otherwise I could do
// personWithMixedAge.age = 'foo';

Your try flow link, but with all read-only and no errors

>All comments

I know that covariance and contravariance can sometimes cause similar confusion, but I don’t see why it would change my expectations here.

Here is why:

const arrayOfNumbers: Array<number> = [1, 2, 3];
const arrayOfMixed: Array<mixed> = arrayOfNumbers;
arrayOfMixed[0] = 'foo'; // allowed for `Array<mixed>` but violates `Array<number>`

Because arrayOfMixed is allowed to be mutated and because it is referentially the same array as arrayOfNumbers it is possible that line 3 could occur and is thus an error to allow line 2. There are a couple ways around this.

// use spread to create a new reference which can be mutated without affecting the `arrayOfNumbers`
const arrayOfMixed: Array<mixed> = [...arrayOfNumbers];

// use a read-only array to prevent mutation
const arrayOfMixed: $ReadOnlyArray<mixed> = arrayOfNumbers;

In your follow up examples:

// mixed works when it’s a top-level value
const num: number = 3;
const mix: mixed = num;
// This is allowed because they are constants and can never be reassigned or mutated.
// mixed FAILS inside object types
const personWithNumberAge: { age: number } = { age: 55 };
const personWithMixedAge: { age: mixed } = personWithNumberAge;
// This fails otherwise I could do
// personWithMixedAge.age = 'foo';

Your try flow link, but with all read-only and no errors

Was this page helpful?
0 / 5 - 0 ratings