question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Inconsistent Type Inference of Object Property In Union Type

See original GitHub issue

Bug 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 is boolean, "a" | "b", null or even `x${string}`, the type of x as number is correctly inferred when arg is set to 0.
  • When T is string, never, number[], inference of x as type number 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:open
  • Created a year ago
  • Reactions:6
  • Comments:8 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
fatcerberuscommented, Apr 4, 2022

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 the string | 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.

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.

1reaction
sullivan-seancommented, Apr 1, 2022

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:

The rule for providing the contextual type on the function is basically:

  • Check the target type for a discriminant property (technically one or more discriminant properties)
  • If you find one, see if the source type has a matching property
  • If you find that, filter the target type based on the matching constituent of the target union, and if this produces exactly one result, use that to contextually type the source expression

It is now much easier for me to explain why the following behavior occurs:

type Closure<T> = { arg: T, fn: (_: T) => void };


const a: Closure<null> | Closure<string> | Closure<number> = {
  arg: 0,
  // x can be inferred as number because arg has target type `null | string | number` and source type `number`
  // the target type has a literal `null` and is therefore discriminant
  fn: (x) => {}
}

const b: Closure<string> | Closure<number> = {
  arg: 0,
  // x cannot be inferred as number because arg has target type `string | number` and source type `number`
  // the target type has no literal and is therefore *not* discriminant
  fn: (x) => {}
}

I think it had originally just seemed counterintuitive that the correct contextual type can be selected by comparing the number source type with target type null | string | number, but not with target type string | 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 the string | 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Type union not checking for excess properties - Stack Overflow
This is a bit of a quirk in how unions work in conjunction with excess property checks. {A:1, C: 3} is actually compatible...
Read more >
Troubleshooting F# | F# for fun and profit
F. Inconsistent return types in branches or matches ... A common mistake is that if you have a branch or match expression, then...
Read more >
Documentation - TypeScript 2.8
Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y...
Read more >
Common issues and solutions - mypy 0.991 documentation
The function containing the error is not annotated. Functions that do not have any annotations (neither for any argument nor for the return...
Read more >
Python Type Checking (Guide) - Real Python
Instead you check for the presence of a given method or attribute. As an example, you can call len() on any Python object...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found