Reputation: 828
I would like to learn how Flow decides what type to use for a generic type, and if there is a way to control at what level the generic type gets inferred (what I mean by this is explained further down).
This question is inspired by How to type a generic function that returns subtypes. I think there is a distinction between the two questions because this one focuses on understanding how
T
is chosen, where as the linked on is focuses on typing the return type of a function.
The identity function is a great example to dissect. Its type is fairly straightforward
function identity<T>(value: T): T;
This seems like enough information to know what the implementation should be. However, I feel like this type is insufficient to know what the identity function actually does. For example, we could have (as the linked question tries to do),
function identity<T>(value: T): T {
if (typeof value === 'string') {
return '';
}
return value;
}
This does not typecheck, with Flow complaining about returning the empty string. However, I would imagine in many languages that this would be fine--we are returning a string
when a string
was inputted, otherwise we are returning the original value
of type T
--but for some reason Flow does not like this.
My confusion is compounded by both this answer, where we can return value.substr(0, 0)
instead of the empty string and Flow will no longer complain, and by the inability to return a strictly equal value,
function identity<T>(value: T): T {
if (value === '') {
return '';
}
return value;
}
I think a major reason for this discrepancy is that literals can act like types in Flow, in addition to the "JavaScript type". For example,
const x: 5 = 5; // literal type
const x: number = 5; // JavaScript type
are both valid. However, this means that when we have a function of type T => T
, we do not know if Flow is inferring the literal or JavaScript type as the type.
I would like to know if there is some way of either knowing what Flow infers for generic types in a function or if there is a way to scope the generic type to be at the "literal" level or "JavaScript" level. With this ability, we could type function that coerces values to the default value for that type (i.e., strings would go to the empty string, numbers would go to 0). Here the type of the function would effectively be T => T
, but hopefully Flow could be prevented from complaining about returning the default values.
Upvotes: 4
Views: 582
Reputation: 1304
Hoping to shed a little light here on what's going on, if not answer the question directly.
Let's take your first example first of all:
function identity<T>(value: T): T {
if (typeof value === 'string') {
return '';
}
return value;
}
The function signature is identity<T>(T): T
. This is basically saying:
T
which could be anything (<T>
).T
.T
.From this point forward, none of these restrictions are going to change, and the type of T
is also not going to change. identity
must return the exact type of T
, not a subset of its type. Let's look at why.
identity<'some string'>('some string');
In this case the type of T
is the literal type, 'some string'
. In the case of this invocation of the above function, we would find that typeof value === 'string'
and attempt to return ''
, a string
. string
, however, is a supertype of T
which is 'some string'
, so we have violated the contract of the function.
This all seems rather contrived in the case of simple strings, but it's actually necessary, and much more obvious when scaling up to more complex types.
Let's look at a proper implementation of our weird identity function:
function identity<T>(value: T): T | string {
if (typeof value === 'string') {
return '';
}
return value;
}
A return type of T
can only be satisfied by something which exactly matches T
, which in the case of our signature can only be value
. However, we have a special case where identity
may return a string
, so our return type should be a union of T | string
(or, if we wanted to be super specific, T | ''
).
Now let's move on to this second example:
function identity<T>(value: T): T {
if (value === '') {
return '';
}
return value;
}
In this case, flow just doesn't support value === ''
as a refinement mechanism. Refinement in flow is very picky, I like to think of it as a list of a few simple regular expressions that are run over my code. There's really only way to refine the type to a string, and that's by using typeof value === 'string'
. Other comparisons won't refine to string. There's definitely also some wonkiness around refining generics, but something like this works fine (the refinement does, it still exhibits the previous generic-related error, of course):
function identity<T>(value: T): T {
if (typeof value === 'string' && (value: string) === '') {
return '';
}
return value;
}
(Try)
As for the substr
example, that definitely looks like a bug to me. It seems you can do the same with any method on String
that returns a string
, such as concat
or slice
.
I would like to know if there is some way of either knowing what Flow infers for generic types in a function
Within the function body flow doesn't really infer the type of a generic. A generic has a concrete definition (T
is T
, essentially an unknown type, unless it has bounds, in which case it is an unknown type that matches those bounds). Flow may infer the types of parameters going into invocations of the function, but that should have no bearing on how the functions are written.
or if there is a way to scope the generic type to be at the "literal" level or "JavaScript" level. With this ability, we could type function that coerces values to the default value for that type (i.e., strings would go to the empty string, numbers would go to 0). Here the type of the function would effectively be T => T, but hopefully Flow could be prevented from complaining about returning the default values.
The problem here is that this would no longer be T => T
. As I've shown above, breaking such an implementation is trivial.
Upvotes: 7