Inconsistent Type Inference of Object Property In Union Type
See original GitHub issueBug Report
🔎 Search Terms
parameter ‘x’ implicitly has type ‘any’ type inference discriminated union discriminant property type narrowing of parent object
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ
⏯ Playground Link
Playground link with relevant code
💻 Code
type Closure<T> = { arg: T, fn: (_: T) => void };
const a: Closure<`x${string}`> | Closure<number> = {
arg: 0,
// x is inferred as number, because of arg above
fn: (x) => {}
}
const b: Closure<string> | Closure<number> = {
arg: 0,
// x cannot be inferred (parameter 'x' implicitly has type 'any')
fn: (x) => {}
}
🙁 Actual behavior
Inference of the parameter to the function field fn
is inconsistent across the two objects. The only difference is the type T
in Closure<T> | Closure<number>
for the object type:
- When
T
isboolean
,"a" | "b"
,null
or even`x${string}`
, the type ofx
asnumber
is correctly inferred whenarg
is set to0
. - When
T
isstring
,never
,number[]
, inference ofx
as typenumber
is not possible.
See TS playground link for more examples of which types inference is possible for.
There is no immediately obvious pattern as to what types result in possible inference and what types preclude it.
I’m no expert on how this inference is accomplished, but if it is at all similar to type narrowing then this comment https://github.com/microsoft/TypeScript/issues/30506#issuecomment-474802840 is the closest explanation I can find. Maybe type inference works when T
is boolean ~ true | false
and "a" | "b"
because arg
is then discriminant property. Even in this case, it would seem a field of type `x${string}`
would be a discriminant property(?) and I can’t wrap my head around why a field of type string
could not be a discriminant property if one of type `x${string}`
can be
🙂 Expected behavior
Type inference of the parameter x
to property fn
is consistent across types T
in Closure<T> | Closure<number>
when arg
is set to 0
and 0
is not an instantiation of T
OR
If type inference is not meant to be consistent due to a design limitation (e.g. as in https://github.com/microsoft/TypeScript/issues/33205#issuecomment-528182920), this behavior difference and any associated design limitation is better/more clearly documented – I’d love to see something on discriminant properties in official documentation, as I found the Github threads a little hard to follow
Issue Analytics
- State:
- Created a year ago
- Reactions:6
- Comments:8 (4 by maintainers)
Top GitHub Comments
Yep, and if some of those discriminants are object types themselves, things get even more hairy, since then the checker would have to recurse into nested objects too. The performance picture would get ugly fast. There are often requests to have discriminated unions work when the discriminant property is nested 2 or 3 levels deep - this is why that doesn’t work.
Thanks, this has cleared up a lot of confusion around discriminant properties for me. I’d love to see more in the official documentation describing them – I think having a better understanding of discriminant properties makes the notion of consistency here much more apparent 😄
I found this particularly helpful:
It is now much easier for me to explain why the following behavior occurs:
I think it had originally just seemed counterintuitive that the correct contextual type can be selected by comparing the
number
source type with target typenull | string | number
, but not with target typestring | number
. This is unintuitive because the former seems more difficult/less performant to narrow since we are inferring from a larger union of types.From your last comment I think that actually, it IS more difficult/less performant to select the contextual type in the
null | string | number
case than thestring | number
case as intuition suggests BUT if we were to turn on discriminant properties for all properties, we would just end up doing WAY more of these computations on every union of objects with properties and that would be prohibitively expensive.