Flow: Generic type using bounded polymorphism incompatible with third party lib def

Created on 27 Jun 2018  Â·  6Comments  Â·  Source: facebook/flow

Hi, first of all, thanks for this project! Types help catch some really obscure bugs sometimes.

I've been playing around with generics lately and I wanted to pass a value returned from a function to a third-party library with type definitions (the definition specifies a type of an array of primitives). Flow throws an incompatible error while comparing the arrays of generic and the primitive types.

Is there some sort of casting or something I need to do to make something like this work? Or am I totally on the wrong track?

const getArr = <T: number>(obj: {numbers: Array<T>}) => obj.numbers;
const arr = getArr({numbers: [1, 2, 3]});
processArr(arr);

// Third party lib def
function processArr(arr: Array<number>) {}
3: processArr(arr)
              ^ Cannot call `processArr` with `arr` bound to `arr` because `T` [1] is incompatible with number [2] in array element.
References:
1: const getArr = <T: number>(obj: {numbers: Array<T>}) => obj.numbers;
                                                   ^ [1]
6: function processArr(arr: Array<number>) {}
                                  ^ [2]

https://flow.org/try/#0MYewdgzgLgBA5gUygQQE6pgXhgHgCoBcMYArgLYBGCqAfABQgUBWRA3qZdREWqgIYBPfDQC+ASiw0YjJgDoOVVBADcAKFCRYfdFnhJedduUXcYAbQCMAGhgAmGwGYAuuLUAHVCGAIIEA9tQxNVUAehCYPAALAEtUABMYN20oARgAG2iKGDiEADNVXJIwYCho8ETPb19-dB50QRwFahoJVhFVIA

Originally mentioned in my comment in https://github.com/facebook/flow/issues/1395#issuecomment-400506495

Most helpful comment

Sure; I’m happy to explain.

I’m going to replace the names in your example to make things a bit less
confusing (like numbers: Array<string>). I’m also going to assume that
you meant for processArr to take an Array<string>. So, we have the
following example
;

function getArr<T: string>(obj: {strings: Array<T>}): Array<T> {
  (obj.strings: Array<string>); // fails
  processArr(obj.strings); // fails
  return obj.strings;
}
declare var o: {strings: Array<string>};
const arr: Array<string> = getArr(o);  // works (good)
function processArr(arr: Array<string>) {}

Here, Flow complains that even though T is a subtype of string, it
is not the case that Array<T> is a subtype of Array<string>. This
may be surprising the first time that you hear it, but it is actually
completely necessary, for the following reason.

Suppose that processArr were defined as follows:

function processArr(arr: Array<string>) {
  arr.push("hello");
}

Now, let’s suppose that I have an enumerated type of colors represented
by strings, plus an array of some colors:

type Color = "red" | "green" | "blue";
const colors: Array<Color> = ["red", "green"];

No problems so far. Note that Color is certainly a subtype of string.
But if Array<Color> were a subtype of Array<string>, then we could
write

const colors: Array<Color> = ["red", "green"];
const strings: Array<string> = colors;
processArr(strings);

But then colors would contain the element "hello", which is not a
color! We would have violated type safety.

The same problem occurs with other kinds of subtypes. A classic example
is to consider class Animal and class Dog extends Animal. If we
could pass an Array<Dog> to a function expecting an Array<Animal>,
then the function could add a Cat to our array—and then the client
would be confused because it expected the array to only contain Dogs.

The problem is that if we can treat an array of type T as an array of
type S for some supertype S, then we can insert values of type S
into the array—then, our array might contain things other than Ts.

Note that this problem only occurs when the array is mutated. If the
array is read-only, there’s no problem. This code works:

function getArr<T: string>(
  obj: {strings: $ReadOnlyArray<T>}
): $ReadOnlyArray<T> {
  (obj.strings: $ReadOnlyArray<string>); // works!
  processArr(obj.strings); // works!
  return obj.strings;
}
declare var o: {strings: $ReadOnlyArray<string>};
const arr: $ReadOnlyArray<string> = getArr(o);  // works (good)
function processArr(arr: $ReadOnlyArray<string>) {}

Does this make sense?


Here is some terminology that you might see floating around this area.
We say that the $ReadOnlyArray type constructor is _covariant_ because
if T is a subtype of S, then $ReadOnlyArray<T> is a subtype of
$ReadOnlyArray<S>. It’s covariant because the subtyping relation
varies _with_ the type parameter. On the other hand, Array is
_invariant_, because this is not the case.

Flow indicates that a type constructor is covariant in a type parameter
by marking the type parameter with a +-sign, like this:

class $ReadOnlyArray<+T> { /* … */ }

All 6 comments

This is strange. The problem doesn’t require a third-party libdef; you
can reproduce it as follows:

function getArr<T: number>(obj: {numbers: Array<T>}) {
  return obj.numbers;
}
declare var o: {numbers: Array<number>};
const arr: Array<number> = getArr(o);  // error?!

If we ask flow type-at-pos the type of getArr, it says:

<T: number>(obj: {numbers: Array<T>}) => Array<T>

which is correct. If we explicitly annotate the return type of getArr…

function getArr<T: number>(obj: {numbers: Array<T>}): Array<T> {
  return obj.numbers;
}
declare var o: {numbers: Array<number>};
const arr: Array<number> = getArr(o);  // works (good)

then the problem goes away.

It’s true that Array<T> is not and should not be a subtype of
Array<number> (if you’re not sure why, let me know and I can explain),
but it’s not clear why that is relevant here. As expected, replacing
Array with $ReadOnlyArray everywhere fixes the problem.

Unless I’m missing something, this looks like an inference bug.

Hey @wchargin, thanks for your answer. Would be happy to hear any explanations you can provide on generics! I'm pretty new to a lot of these concepts.

Cool that the error goes away outside of the function scope with return type annotations.

However, I now realize I may have not provided a full reproduction of the use case. The reason I was using the function is because I was calling it from within getArr, which also fails with the return type annotation:

function getArr<T: number>(obj: {numbers: Array<T>}): Array<T> {
  (obj.numbers: Array<number>); // fails
  processArr(obj.numbers); // fails
  return obj.numbers;
}
declare var o: {numbers: Array<number>};
const arr: Array<number> = getArr(o);  // works (good)
function processArr(arr: Array<number>) {}

Sure; I’m happy to explain.

I’m going to replace the names in your example to make things a bit less
confusing (like numbers: Array<string>). I’m also going to assume that
you meant for processArr to take an Array<string>. So, we have the
following example
;

function getArr<T: string>(obj: {strings: Array<T>}): Array<T> {
  (obj.strings: Array<string>); // fails
  processArr(obj.strings); // fails
  return obj.strings;
}
declare var o: {strings: Array<string>};
const arr: Array<string> = getArr(o);  // works (good)
function processArr(arr: Array<string>) {}

Here, Flow complains that even though T is a subtype of string, it
is not the case that Array<T> is a subtype of Array<string>. This
may be surprising the first time that you hear it, but it is actually
completely necessary, for the following reason.

Suppose that processArr were defined as follows:

function processArr(arr: Array<string>) {
  arr.push("hello");
}

Now, let’s suppose that I have an enumerated type of colors represented
by strings, plus an array of some colors:

type Color = "red" | "green" | "blue";
const colors: Array<Color> = ["red", "green"];

No problems so far. Note that Color is certainly a subtype of string.
But if Array<Color> were a subtype of Array<string>, then we could
write

const colors: Array<Color> = ["red", "green"];
const strings: Array<string> = colors;
processArr(strings);

But then colors would contain the element "hello", which is not a
color! We would have violated type safety.

The same problem occurs with other kinds of subtypes. A classic example
is to consider class Animal and class Dog extends Animal. If we
could pass an Array<Dog> to a function expecting an Array<Animal>,
then the function could add a Cat to our array—and then the client
would be confused because it expected the array to only contain Dogs.

The problem is that if we can treat an array of type T as an array of
type S for some supertype S, then we can insert values of type S
into the array—then, our array might contain things other than Ts.

Note that this problem only occurs when the array is mutated. If the
array is read-only, there’s no problem. This code works:

function getArr<T: string>(
  obj: {strings: $ReadOnlyArray<T>}
): $ReadOnlyArray<T> {
  (obj.strings: $ReadOnlyArray<string>); // works!
  processArr(obj.strings); // works!
  return obj.strings;
}
declare var o: {strings: $ReadOnlyArray<string>};
const arr: $ReadOnlyArray<string> = getArr(o);  // works (good)
function processArr(arr: $ReadOnlyArray<string>) {}

Does this make sense?


Here is some terminology that you might see floating around this area.
We say that the $ReadOnlyArray type constructor is _covariant_ because
if T is a subtype of S, then $ReadOnlyArray<T> is a subtype of
$ReadOnlyArray<S>. It’s covariant because the subtyping relation
varies _with_ the type parameter. On the other hand, Array is
_invariant_, because this is not the case.

Flow indicates that a type constructor is covariant in a type parameter
by marking the type parameter with a +-sign, like this:

class $ReadOnlyArray<+T> { /* … */ }

Thanks for the explanation!

The example with Color vs string makes sense to me. But I suppose what is still confusing is why Flow doesn't treat generics as aliases to their types defined within the <>. For example, why T is not treated as an alias to string here:

function getArr<T: string>(obj: {strings: Array<T>}): Array<T> {}
declare var o: {strings: Array<string>};
const arr: Array<string> = getArr(o);

I’m going to replace the names in your example to make things a bit less
confusing

Oh oops, that last example I posted mixed string and number, my mistake. I've fixed that.


So I guess the only real bug here is (possibly) the return type inference?

For example, why T is not treated as an alias to string here:

The constraint <T: string> doesn’t mean that T must be string; it
means that T may be _any subtype_ of string. It is not an alias; it
is an upper bound.

So, in particular, T could be string, or Color, or "wat", or
empty.

Does this answer your question?

So I guess the only real bug here is (possibly) the return type
inference?

Yep: the return type inference still seems buggy to me.

Ah ok, got it, thanks again!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Beingbook picture Beingbook  Â·  3Comments

funtaps picture funtaps  Â·  3Comments

davidpelaez picture davidpelaez  Â·  3Comments

jamiebuilds picture jamiebuilds  Â·  3Comments

philikon picture philikon  Â·  3Comments