Flow: Document how to best type generic functions that return a subtype of the input type

Created on 25 Feb 2019  Â·  16Comments  Â·  Source: facebook/flow

As per flow's documentation, generics in flow tracks values around.

That means that this will raise an error:

function identity<T>(value: T): T {
  if (typeof value === 'string') {
    // $ExpectError
    return '';
  }

  return value;
}

As far as I can tell, the documentation does not mention how to best type a function like this. Given that flow has first class support for generics, I'm assuming there is a way to specify that the function returns a sub-type of the input type. Specifically a way to avoid less safe typings like these:

function identity<T>(value: T): (T | string) {
  ...
}

or

function identity(value: mixed): mixed {
  ...
}

If for some reasons there is no way to accomplish this with flow, I think it would also be valuable to put that in the documentation. I've spent a multiple hours try to figure this one out.

Thanks!

documentation

Most helpful comment

I'm assuming there is a way to specify that the function returns a
sub-type of the input type

But your function does _not_ do that:

type Color = "red" | "blue";
const x: Color = "red";
const y: Color = identity(x);  // hmm...

Your function (called identity, but not the identity function) may
return the empty string, which is not a member of arbitrary subtypes of
string.

There is no (intended) way to convince Flow that your “identity”
function has the type <T>(T) => T, because that would simply be
unsound.

If you really _do_ have a value whose type is a subtype of T, then you
could just return it. If S is a subtype of T, it is always valid to
provide a value of type S where a value of type T is wanted. This is
what it means to be a subtype.

All 16 comments

Why do you consider

function identity<T>(value: T): (T | string) {
  ...
}

to be unsafe?

I wouldn't call it _unsafe_, but at the very least it's definitely wrong. I too am curious to know how to correctly type this because I've encountered this in the past as well (tangentially related: https://github.com/facebook/flow/issues/7197#issue-382394110).

I'm assuming there is a way to specify that the function returns a
sub-type of the input type

But your function does _not_ do that:

type Color = "red" | "blue";
const x: Color = "red";
const y: Color = identity(x);  // hmm...

Your function (called identity, but not the identity function) may
return the empty string, which is not a member of arbitrary subtypes of
string.

There is no (intended) way to convince Flow that your “identity”
function has the type <T>(T) => T, because that would simply be
unsound.

If you really _do_ have a value whose type is a subtype of T, then you
could just return it. If S is a subtype of T, it is always valid to
provide a value of type S where a value of type T is wanted. This is
what it means to be a subtype.

I realize that the name identity is confusing. I didn't mean to name it that way - I tweaked an example and I forgot to rename it. Sorry about that.

@dsainati1 I would call it less safe because the return type is not as fine as it could be and forces callers to add un-needed type refinements at run time to be safe. For example2 is safer than example1.

// works but number is not needed
function example1(str : string) : string {
  return str;
}
// better
function example2(str : string) : string | number {
  return str;
}

@wchargin

Your function may return the empty string, which is not a member of arbitrary subtypes of string.

I'm not sure I follow why an empty string is not an "arbitrary subtype" of string since ('' : string) is valid. Can you give me an example of what would be an "arbitrary subtype" of string?

Just to confirm, are you saying that the best way to type a function is this?

function func<T>(value: T) : (T | string) {
  if (typeof value === 'string') {
    return '';
  }

  return value;
}

If that's case, it seems not ideal that the return type of func<number> is (string | number) when clearly string cannot be returned. Is there a way with flow to type a function to say that function will return a value that is the subtype of its input value when using generics?

Let me know if you'd like me to post a less contrived example where I feel I cannot express this naturally occurring type constrain.

@jnak The thing to notice is that passing the typeof value === 'string' check doesn't actually mean that the type of value is string; there is a difference between Flow's static types and the types that exist at runtime for the purposes of checks like these. Consider what might happen if this code were to pass Flow:

type Enum = "A" | "B";
function identity<T>(value: T): T {
  if (typeof value === 'string') {
    return '';
  }
  return value;
}
let x : Enum = "A";
let y : Enum = identity<Enum>(x);

In this case, x would pass the conditional check and thus we would return '' from the function, but '' isn't a subtype of Enum, and so y would have an unsound type .

I'm not sure I follow why an empty string is not an "arbitrary
subtype" of string since ('' : string) is valid. Can you give me
an example of what would be an "arbitrary subtype" of string?

Sure; let me be more explicit. It is not the case that, for _every_ type
T that is a subtype of string, "" is a value of type T. For
instance, "red" | "blue" is one subtype of string, but "" is not a
value of type "red" | "blue".

Your generic function needs to work for _all_ T. Like @dsainati1 said,
just because typeof x === "string" and x: T does not mean that T
is the type string; it merely means that T is a subtype of string.

@jnak you can use function overloading

declare function func(value: string): string
declare function func<T>(value: T): T
function func(value: mixed) {
  if (typeof value === 'string') {
    return '';
  }

  return value;
}

@goodmind: Please don’t use declare in that way. What you have written
gives func the unsound type of

((value: string) => string) & (<T>(value: T) => T)

whence an unsuspecting caller may freely write

function bad<T>(value: T): T {
  return func(value);
}
const wat: "wat" = bad("wat");  // unsound!

and we’re back where we started.

@wchargin @dsainati1 Thanks for answering. It looks like I tried to simplify my actual issue too much and that ended up being more confusing than helpful. My actual issue was a "typed" generic:

function func<T : string | number>(value: T): T {
  if (typeof value === 'string') {
    return '';
  }
  return value;
}

Here I was hoping to avoid typing it like func<T : string | number>(value: T): T | string because func<string> can only return string and func<number> can only return number. Can you confirm to me that there is no better workaround for this type of situations?

function func<T : string | number>(value: T): T {
  if (typeof value === 'string') {
    return '';
  }
  return value;
}

Has the exact same issue as before.

let y : "A" = func<"A">("A")

is unsound.
If you want to have two different functions for number and string, this would be a case for overloading:

declare function func(v : number) : number;
declare function func(v : string) : string;

I know this is contrived, but of curiosity, why can't I do this?

function func<T>(value: T): T {
  if (typeof value === 'string') {
    return String(value);
  }
  return value;
}

Has the exact same issue as before.

Yes, and to be a bit more explicit: the bound <T: string | number>
does _not_ mean that T must be either the type string or the type
number; it means that T must be a subtype of string | number, and
that it may be _any_ such subtype, including "red" | "blue".

I know this is contrived, but of curiosity, why can't I do this?

function func<T>(value: T): T {
  if (typeof value === 'string') {
    return String(value);
  }
  return value;
}

That _is_ sound (I believe), but the type of String(…) as declared in
the standard library is not precise enough to show it:

https://github.com/facebook/flow/blob/2c77038221739f9eab7b52b4383b7de0d815f9af/lib/core.js#L331

For your example to typecheck, String would need to have a type more
like “<T>(T) => (T <: string ? T : string)”, which isn’t really
expressible in Flow. You _might_ be able to hack something up with the
SFINAE-like semantics that I demonstrate in #6633, but as I mention in
that issue I wouldn’t be comfortable putting that in the standard
library without a few people more knowledgeable than me taking a good
hard look at it.

Okay sure, that does appear to be a problem with core.js. But my concern here is that the problem goes a little deeper than that.

Take this example instead:

function identity<T>(value: T): T {
  if (value === null) {
    return null;
  }
  return value;
}

Or this one:

function identity<T>(value: T): T {
  if (typeof value === 'undefined') {
    return undefined;
  }
  return value;
}

My point is that there's something else going on here that doesn't feel quite right.

Wait, something else is going on here...

function identity<T>(value: T): T {
  if (typeof value === 'string') {
    return value + 0;
  }

  return value;
}

EDIT: I had my coercion backward, this is what I meant:

function identity<T>(value: T): T {
  if (typeof value === 'number') {
    return value + 'foo';
  }

  return value;
}

Take this example instead:

Ah, I see. Yes. What’s happening there is that inside the if-statement
Flow refines value to be of type null but does not refine the type
parameter T to indicate that it must include the null value. This
kind of bidirectional type refinement is something that would be a
convenient addition. I don’t know how feasible it is in terms of
implementation.

Wait, something else is going on here...

This is https://github.com/facebook/flow/issues/7070.

OK yeah, it's the lack of bidirectionality in the type refinement that's messing with me. Gonna leave another example of this weird behavior just in case it helps anyone else see how weird that can get:

function identity<T>(value: T): T {
  if (typeof value === 'number' && value === 0) {
    return value;
  }

  return value;
}
2:   if (typeof value === 'number' && value === 0) {
                                                ^ number literal `0` [1] is incompatible with `T` [2].
References:
2:   if (typeof value === 'number' && value === 0) {
                                                ^ [1]
1: function identity<T>(value: T): T {
                               ^ [2]
Was this page helpful?
0 / 5 - 0 ratings

Related issues

glenjamin picture glenjamin  Â·  3Comments

marcelbeumer picture marcelbeumer  Â·  3Comments

Beingbook picture Beingbook  Â·  3Comments

tp picture tp  Â·  3Comments

jamiebuilds picture jamiebuilds  Â·  3Comments