-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type inference is fuzzy / blackbox-y #16774
Comments
Can you simplify your examples to not depend on things from the |
@RyanCavanaugh Apologies, it was a long evening. I've updated the code. |
It is definitely deterministic. As for predictable, well, it's generic type inference and I can't guarantee anyone will be able to keep the entire program state in their head as well as a computer can. A short version of the inference process is:
In your case, the problem is here: [K in keyof O]: O[K] | Data<O[K]> | (() => Data<O[K]>) | (() => O[K])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Both of those function types create inference sites for the corresponding property and we can't figure out which to prefer. A simpler example with a possible fix: class Data<T> {
x: T;
constructor(v: T) { }
}
type Binding1<O> = {
[K in keyof O]: Data<O[K]> |
(() => Data<O[K]>) |
(() => O[K]);
}
type Binding2<O> = {
[K in keyof O]: Data<O[K]> |
(() => (Data<O[K]> | O[K]));
}
declare function bind1<O>(b: Binding1<O>): O;
declare function bind2<O>(b: Binding2<O>): O;
const obj = {
a: () => new Data<number>(123),
b: new Data<boolean>(true)
};
const b1 = bind1(obj);
b1.a.toFixed();
const b2 = bind2(obj);
b2.a.toFixed(); Alternatively: class Data<T> {
x: T;
constructor(v: T) { }
}
type Primitive = string | number | boolean
type Bound<O> = {
[K in keyof O]: Primitive
}
type Binding<O> = {
[K in keyof O]: O[K] | Data<O[K]> | (() => Data<O[K]>) | (() => Primitive)
}
function bind<O extends Bound<O>>(b: Binding<O>, c: ((a: O) => {})): null {
return null
}
var MyComponent = bind({
a: () => new Data<number>(123),
b: new Data<boolean>(true)
},
function ({ a, b }) {
var j: number = a;
var l: boolean = b;
return null;
}); |
@RyanCavanaugh You're a legend, thanks for your time and pointers. I still have the feeling inference could be better with those generic constrains. I've simplified a bit further: Basic case: class Data<T> {
constructor(v: T) { }
}
type Binding<O> = {
[K in keyof O]: O[K] | Data<O[K]> | ( () => O[K] | Data<O[K]> )
}
declare function bind<X>(b: Binding<X>): void;
bind({
a: 123,
b: () => 123,
c: new Data(234),
d: () => new Data(234)
}); This now gets inferred as:
So here the matcher already makes some "choices", right (perhaps the part you're referring to as "best candidate" in your answer)? Now, let's extend by adding a Constrain indicating we really want primitive values in X: class Data<T> {
constructor(v: T) { }
}
type Constrain<O> = {
[k in keyof O]: number | string | boolean
}
type Binding<O> = {
[K in keyof O]: O[K] | Data<O[K]> | ( () => O[K] | Data<O[K]> )
}
declare function bind<X extends Constrain<X>>(b: Binding<X>): void;
bind({
a: 123,
b: () => 123,
c: new Data(234),
d: () => new Data(234)
}); Because of the added constraint, the matcher now seems to go in an entirely different direction: Would it be hard for the inference to explore all the types, and properly evaluating In this case:
ergo |
I believe your example there might require multiple passes, which effectively turns it into a unification algorithm. See also #30134 |
TypeScript Version: 2.5.0-dev.20170627
When a generic type has multiple constraints (through
extends
in generic declaration), it is very hard to predict which one the compiler uses for type inference. I would expect this to be deterministic/predictable.Two examples (and excuse the verbosity of the code, found it hard to replicate with less):
Listing 1
MyComponent
is type inferred asbind<{},{a: number, b: boolean}>...
as I would expect. This snippet compiles fine.Listing 2
Binding<..>
now has an additional map value in the form of| (() => O[K])
. For some reason, this triggersMyComponent
to be inferred asbind<{}, { a: string | number | boolean, b: string | number | boolean }>
(which is the type of theBound
). And it then fails to compile because the assignment in the function is incorrect.I can certainly see type inference under these kind of complex conditions is never going to be trivial, but it's really hard to have any clue as to what's happening here.
Does it work like this because of the spec, because (of some exploration bounds in) the implementation? And is there something I can do to force the inference towards the first case?
I'm posting here as to maybe have a more generic discussion about how such complex cases might be made more approachable by normal programmers. If this is better suited on SO, please do close the ticket.
The text was updated successfully, but these errors were encountered: