Reputation: 2281
Is there a way to help the compiler to infer something like the following:
class Base<T> {
children(... children: (A<any> | B<T>)[]) { return this }
onVisit(handler:(context: T)=>void) { return this }
}
class A<T> extends Base<T> {
constructor( public context: T ) { super() }
}
class B<T> extends Base<T> {}
const foo = new A({ bar: 1 })
.children(
new A({baz:2}).onVisit(({baz})=>{}),
new B().onVisit(({bar})=>{}) // Fails here because it infers that the instance as type B<unknown> instead of B<{bar:number}>
)
It doesn't seem to be because the compiler can't draw some context from the calling function since this works:
function f1<T>(p: T, ...h: ((p:T) => void)[]) { }
function f2<T>(h: (p: T) => void) { return h }
f1({ a: 1 },
f2((p) => { console.log(p.a) }),
)
I might be totally confused (very likely) but it seems like if the latter works so should the former.
Upvotes: 3
Views: 2282
Reputation: 327964
"Normal" type inference in TypeScript happens when the compiler examines the contents of a expression to determine what type it is. It looks inside the expression to do this. This tends to happen in the same direction as control flow of the emitted program at runtime. If I have code like
let x = "";
let y = x.length;
, the compiler determines the type of ""
(which will be ""
), and uses that to determine the type of x
(which will be string
). Then the compiler determines the type of x.length
by checking the type of the length
property on string
(which will be number
), and then uses this to determine the type of y
(which will be number
). Earlier expressions determine the types of later expressions.
However, there are a few situations where the compiler does contextual typing. In contextual typing, inference happens when the compiler examines the context of an expression to determine what type it is. It looks outside the expression to do this. This tends to happen in the opposite direction of the control flow of the emitted program at runtime. If I have code like
[""].map(z => z.length)
, the variable z
inside the callback has no explicit typing, but its type cannot be determined by examining the contents of the callback. However, the since the map()
method of an array of string
values expects its argument to be a callback that takes a string
property, the compiler uses this context to give z
the type of string
. Here, in some sense, a later expression is used to determine the type of an earlier expression.
But contextual typing is fragile. It only happens in a few special circumstances, and is easily broken by making the necessary contextual information "father away" from the expression whose type needs to be inferred:
let cb = z => z.length; // error, z is implicitly any
[""].map(cb); // oops, now we have an any[]
Here the same callback, z => z.length
is being created. But it is happening by itself. The only place it is used is as a callback to a string[]
's map()
method. So technically the compiler could conceivably say, "well, cb
is expected to be a callback of a type like (val: string) => any
, and therefore we go up one line and give cb
that type, and then from that context we can infer that z
must be a string
. But this does not happen. Inference fails.
One place contextual typing happens is inferring the type parameter of a called generic function by using the expected return type for context. Your f2()
situation does this, similar to this:
declare function f<T>(): T;
const numGood: number = f(); // infers number for T
The function f()
claims to return a value of any possible type T
. One could never infer T
from a call to f()
in the "normal" way because there's nothing about f()
to tell you what T
should be. But contextually, the compiler expects it to return a number
because numGood
is annotated as number
. And so it works. This actually can be propagated backward through multiple function calls, as long as the type mapping is straightforward:
declare function id<T>(x: T): T;
const numGood: number = id(id(id(id(f())))); // infers number for all Ts
But, in your new B().onVisit
situation, you are trying to make the compiler contextually infer a function's (or, rather, a constructor call's) type parameter given the type of a property (or, rather, a method) of its return value. This apparently breaks contextual inference. It's similar to this:
declare function g<T>(): { a: T };
const numBad: number = g().a; // error!
The function g()
returns a value of type {a: T}
. Even though g().a
is being used in a context that expects a number
, this apparently does not propagate backwards to the call site of g()
. The inference fails. T
defaults to unknown
, and you get an error.
I can't point to a particular GitHub issue or documentation that outlines when you can and cannot expect contextual typing to work. There are near misses, like microsoft/TypeScript#29771 which describes some situations at the limits of compiler's ability to perform contextual typing. But I haven't yet seen a canonical answer that says "f2
yes, new B().onVisit
no".
Barring that, if you run into a situation where type inference fails you, consider manually annotating or specifying the types yourself. If you don't want new B()
to produce a B<unknown>
, then write new B<{bar: number}>()
and it should start working. Tedious? Sure. But at least it works!
Upvotes: 6