I want to use the type constraint <A>
to make sure that function fn
and function fn2
should return the same type A
as the argument output
, which specifies what is type A
.
So I expect the following code to throw error, but didn't get any error.
flow try
declare function convert <A>(
output: A,
fn: (query: string) => A,
fn2: (query: string) => A
): void;
// Expected errors, but didn't get
convert(
1, // <- this value specifies type A to be number
function(a: string) { return 1; },
function(a: string) { return '1'; } // <- here it returns a string which is a wrong type, but flow didn't get it
);
Did I miss anything?
This comment is wrong:
this value specifies type A to be number
Flow is inferring A
to be of type number | string
, as you can verify by doing
declare function convert <A>(
output: A,
fn: (query: string) => A,
fn2: (query: string) => A
): A;
const x: number = convert(
1,
function(a: string) { return 1; },
function(a: string) { return '1'; }
);
Thanks @gabro!
Is there a way to say A
is not an union type?
You mean a union type?
You mean a union type?
Yes a union type.
I want a way to add type constraints so that the type can be either all number
or all string
, but not all number | string
.
Is it possible?
I want a way to add type constraints so that the type can be either all number or all string, but not all number | string.
Is it possible?
No, it's not possible
Maybe with some bounded polymorphism trickery one could express type-level equality (which is what you're asking), but I can't think of a solution right now.
Maybe with some bounded polymorphism trickery
type _ExtractCodomain<B, F: (a: any) => B> = B;
type ExtractCodomain<F> = _ExtractCodomain<*, F>;
declare function convert <B, F: (query: string) => B>(
fn: F,
fn2: (query: string) => ExtractCodomain<F>
): B;
const x = convert(
function(a: string) { return 1 },
function(a: string) { return '1' } // <= string. This type is incompatible with number
)
@gcanti but why does it work? Seems to be a type inference bug
I kinda figured a workaround, which is to use type casting. Also hacky?
flow try
declare function convert <A>(
output: A,
fn: (query: string) => A,
fn2: (query: string) => A
): void;
var convertNumber = (convert :
(output: number,
fn: (query: string) => number,
fn2: (query: string) => number) => void);
// Expected errors
convertNumber(
1, // <- this value specifies type A to be number
function(a: string) { return 1; },
function(a: string) { return '1'; } // <- here did throw the type error
)
@zhangchiqing well, that's the equivalent or writing
declare function convertNumber(
output: number,
fn: (query: string) => number,
fn2: (query: string) => number
): void;
so not very useful as you would need to explicitly enumerate the types you want to work with.
About @gcanti's hack, I need more coffee.
Ok, I got it. Yes, I agree with @vkurchatkin, I wouldn't expect it to happen.
It seems that if you move the type resolution of B
"far enough" the type unification stops before widening to a union type.
Not sure whether this is intended behavior...
Not sure but if F: (query: string) => B
and F
is (query: string) => number
then B <: number
, so B = number
Oh right, it makes sense! The trick is to create a dependency between the type of fn
and the type of fn2
. In the OP's example both types would contribute equally, but in your example F
is inferred before, and that's why it works.
It seems legit!
Well, Flow fails to infer that F is (query: string) => number | string
in this case
Well, given
const x = convert(
function(a: string) { return 1 },
function(a: string) { return '1' } // <= string. This type is incompatible with number
)
the type of function(a: string) { return 1 }
is (a: string) => number
.
The "trick" is to require that type to be resolved before trying to unify it with the type of the other function.
What I mean, is that I think inferring (query: string) => number | string
for fn
would be a mistake.
How is that different from original example? In general type inference should provider lowest possible types that satisfy all constraints. If it's not possible, then the code doesn't type-check. In this case it's definitely possible, so I'd say that type inference fails to do its job
mm, yes, you may be right. It could just be a limitation of the type unification algorithm.
I think what we need here is a way to provide an upper bound on the type parameter at the call location, like convert<string>(output, f1, f2)
.
@samwgoldman Would that in effect be per call location?
I.E. could someone annotate one call with string
and one with number
and both would check?
Then if there was a third call without an annotation would that coalesce somehow with the first two?
@samwgoldman I agree with @jgrund. Type definition at call-site would be lovely for a number of reasons, but I don't see how it would help in this case.
convert<string>
would substitute string
for T
in a single call. convert<number>
would substitute number
for T
, but the two instantiations would not interact. convert
without a parameterization would behave as it does today. None of the calls would interact.
@samwgoldman true that, however it's way more limiting than the desired OP requirement, i.e. make sure that two types are exactly the same, and having the type inferred at the same time.
@vkurchatkin
How is that different from original example? In general type inference should provider lowest possible types that satisfy all constraints. If it's not possible, then the code doesn't type-check. In this case it's definitely possible, so I'd say that type inference fails to do its job
Does that mean that in the following code, Parsed
should be widened to {text: string}|number
and not fail? (Related: #2618)
// parser.js
/* @flow */
type _$ReturnType<B, F: (...args: any[]) => B> = B;
type $ReturnType<F> = _$ReturnType<*, F>;
export default function parser(el: HTMLElement) {
return {text: el.textContent};
}
export type Parsed = $ReturnType<typeof parser>;
// foo.js
/* @flow */
import type {Parsed} from './parser';
const x: Parsed = 9;
I use the pattern in parser.js
a lot and find the current behavior where I can infer the function's own return type in isolation very desirable. Seems like it's potentially useful in this issue too. Could there be a way to specify the specific behavior here?
supporting convert<number>
makes sense to me.
For now, I prefer my workaround, as it's easier to understand and don't have to modify the polymorphic convert
function.
Please feel free to close this issue. Or leave it open for more discussion.
@samwgoldman Thanks for explanation. Seems like that would be generally useful.
@gabro This particular behavior of Flow is a design choice. The reason we infer number | string
here is the same reason we infer number | string
as the return type of the following function:
function f(b) {
if (b) {
return 0;
} else {
return "";
}
}
That is, Flow tries pretty hard to find a type that makes your program valid. If you didn't intend the union return type, an inconsistency will be discovered when you try to use the returned value as either exactly a string or a number.
One way of looking at is: inference assumes the program is correct, and tries to find a type under that assumption. You can use type annotations to make Flow work the "other way," which is why parameterizing the call has the desired effect.
@samwgoldman all clear, thanks.
I'd like to stress that I think unifying types using union types is a wonderful feature (Scala is introducing it in dotty, for example, and I think it's great news). So my comment wasn't aimed to critique the type inference algorithm, at the contrary!
What I'm say is: due to how the type inference works, in order to solve the A == B
problem the OP posed, we would need some extra construct, in order to constrain the inference.
In other terms, this is a simplified example of what the OP wants:
declare function foo<A>(a: A, b: A)
foo(42, 42) // A == number, ok!
foo("foo", "foo) // A == string, ok!
foo(42, "foo") // A == number | string, ok!
// The <A == B> constraint is imaginary syntax
declare function bar<A, B, A == B>(a: A, b: B)
foo(42, 42) // A == B number, ok!
foo("foo", "foo) // A == B string, ok!
// $ExpectError
foo(42, "foo") // A == number != string == B. Flow complains!
The "typelevel equality" constraint is the missing part.
What I didn't understand of your answer is how refining a type at call site can help in the general case where I don't know the specific type I'm expecting, but I just care they are exactly the same one.
@gabro yeah, it would definitely be useful to express constraints like that on type params. This is something we're thinking about, but not near-term stuff.
@samwgoldman got it, thanks! Back to the original problem, should we consider the "hack" proposed by @gcanti (https://github.com/facebook/flow/issues/2630#issuecomment-253854124) a bug? If yes, maybe we should mark it explicitly, or people may start relying on this buggish behavior.
For me when I use a polymorphic type variable in a function the desired functionality is to constraint the type:
function eq<A>(x: A, y:A) : boolean {
return (x === y)
}
eq(1,1) // Ok. A = number
eq(1,true) // Ups, Flow no errors, A = number | boolean
Flow accepts eq(1,true)
, but my expectation is that Flow rejects that expression,
in this case, for me, the type is useless, equivalent to
function eq(x: any, y:any) : boolean {
return (x === y)
}
Also, I can't check in functions things related, like if a key of type Key<A>
is related whit an object of type A
, etc.
function update<A>(key : Key<A>,elem : A) {
...
}
Additionally, is strange that behavior of polymorphic type variables are different in classes (invariant) than in functions (unifying with union types):
class Example<A> {
eq(x : A, y: A) : boolean { return (x === y) }
}
let x : Example<number> = new Example(); // A instantiated to number
let eqNumber = x.eq // eqNumber = eq(x : number,y : number)
eqNumber(1,1) // A == number, OK
eqNumber(1,true) // A == number, boolean != number, REJECTED.
Creating and instantiating polymorphic classes solves the problem allowing instantiation of the type variable, but I don't like this solution. For me, invariant type variables is the desired behavior and maybe special notation for union types unification, like variant and covariant case, is more intuitive and clear for programmers.
@gcanti 's trick boils down to just:
type Exact<T> = T;
Then you can use it in
function eq<A>(x: A, y:Exact<A>) : boolean {
return (x === y)
}
or in the original case of function return types.
But beware of subtyping, isSame({a:1}, {a:1, b:2});
will typecheck, but isSame({a:1, b:2}, {a:1});
not.
Look like Exact
just bypasses type infer and making things work.
Following code will even work for isSame({a:1, b:2}, {a:1});
case.
const strictEq = <T>(one: T, two: Exact<$Exact<T>>) => one === two;
How to compare first argument in functions? I try $Exact, but it doesn't work for function arguments.
function a(v: string, s: number) {}
function b(v: number, s: string) {}
function c(v: string, s: string) {}
declare function f<Arg>(
fn1: (arg: Arg, ...args: any[]) => any,
fn2: (arg: $Exact<Arg>, ...args: any[]) => any
): any;
// $ExpectError
f(a, b)
// No error
f(a, c)
@theKashey
const strictEq = <T>(one: T, two: Exact<$Exact<T>>) => one === two;
will complain about strictEq(0, 1)
which should pass typecheck
Most helpful comment