TL;DR the compiler uses heuristics to prioritize different inference sites, ultimately inferring the type from the site with the highest priority.
In general, TypeScript determines a specific type for a type parameter (denoted as R
in all examples) by analyzing each inference site, where the type parameter appears in the expression being matched. For instance, in
type P = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
// ^^^^^^^ <-- inference sites --> ^^^^^^^
there are two inference sites for the type parameter R
. The goal of the compiler is to reconcile StringConstructor
with the entire expression by examining an inference site, proposing a potential specific type for that site, and then evaluating the full expression for compatibility with the proposed type.
Let's consider the example above with type P
and speculate on the outcomes when different inference sites are inspected.
If the compiler selects the first inference site for inspection:
type P = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
// ^^^^^^^ <-- inspect this
In this scenario, the inferred candidate would be string
, since String("hello")
results in a string
output. Subsequently, it confirms if string
satisfies the complete expression. Since StringConstructor
indeed extends
(() => string) | { new(...args: any[]): (string & object) }
by extending the initial union member,
R
is deduced as
string
if only the first inference site is considered.
What about the second inference site?
type P = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
// inspect this --> ~~~~~~~~
In this case, the derived candidate would be String
, given that new String("hello")
produces a String
result. It then verifies if String
aligns with the entire expression. As StringConstructor
does extend
(() => String) | { new(...args: any[]): (String & object) }
by extending both sides of the union,
R
is inferred as
String</code if the compiler isolates the second inference site.</p>
<hr />
<p>It may also be plausible for the compiler to contemplate <em>both</em> inference sites simultaneously, merging them based on variance into a union/supertype or intersection/subtype of the candidates. In this context, as the parameters are covariant, I'd speculate that <code>string | String
or simply
String
(being a supertype of
string
) could be potential outcomes.
Consequently, one might envisage string
, String
, or string | String
as feasible results for the aforementioned expression. But what actually transpires?
type P = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
// string
The outcome is string
. This indicates that the compiler accords prioritization to the first inference site. A contrasting scenario unfolds in the subsequent example:
type O = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R) } ? R : never
// String
Here, the compiler favors the second inference site instead. Evidently, (infer R & object)
holds lower precedence compared to just infer R
.
How does the compiler allocate priorities to diverse inference sites? Regrettably, I lack comprehensive insight into this aspect.
Historically outlined in the TypeScript Specification document, which is now archived due to obsolescence, details regarding this process were once available. Current updates outpace formal documentation frequency, rendering such information elusive.
Ongoing discourse within GitHub addresses the notion of inference site prioritization. Visit various issues like microsoft/TypeScript#14829 concerning request features enabling priority nullification at a site, along with challenges arising from developer expectations misaligning with compiler behavior indicated in issues such as microsoft/TypeScript#39295 and microsoft/TypeScript#32389.
A recurring theme notes intersections like (T & {})
possessing inferior precedence in comparison to non-intersecting types like T
. By utilizing T & {}
effectively, the precedence of an intervening site can be diminished when interference occurs. Hereby, elucidating why infer R & object
fails to secure the role of site selection for P
.
Thus, comprehending the exhaustive mechanisms governing these nuances might not always yield enlightening revelations. While scrutinizing the type checker code, assumptions relating to hierarchies like whether return types from construct signatures enjoy higher accord over those from call signatures could be discerned. However, anchoring programs heavily reliant on pronounced intricacies thereof is ill-advised, considering fluctuating inference rules across successive language iterations.
Experience the code in Playground