TypeScript: Inconsistent inference behaviour on union types
The behaviour of ternary operator inference (and similarly if-else & switch-case) seems to be inconsistent between types
TypeScript Version: Tested on 3.7+ up to nightly
Search Terms: union, union inference, inconsistent inference
Code
class ErrorA extends Error {
readonly _tag = "ErrorA"
}
class ErrorB extends Error {
readonly _tag = "ErrorB"
}
interface X<E> {
e: () => E
}
const A = { e: () => "a" as const }
const B = { e: () => "b" as const }
const C = { e: () => new ErrorA() }
const D = { e: () => new ErrorB() }
const E = { e: () => ({a: "0"}) }
const F = { e: () => ({b: "1"}) }
declare const someBoolean: boolean
declare function fn<E>(x: X<E>): E
Expected behavior:
const OK = fn(someBoolean ? A : B) // inferred as "a" | "b"
const NO = fn(someBoolean ? C : D) // inferred as ErrorA | ErrorB
const NO_2 = fn(someBoolean ? E : F) // inferred as {a:string} | {b:string}
Actual behavior:
const OK = fn(someBoolean ? A : B) // inferred as "a" | "b"
const NO = fn(someBoolean ? C : D) // doesn't compile
const NO_2 = fn(someBoolean ? E : F) // doesn't compile
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 8
- Comments: 18 (8 by maintainers)
@RyanCavanaugh For that particular example I don’t think you can soundly infer a union since
findmight mutate the array. But I get your point when the function is defined asWe have a few ad hoc rules in place, such as always unioning literal types from the same domain, but otherwise, when no inference candidate is a supertype of all other inference candidates, we simply pick the first (for some meaning of first) candidate and leave it to the user to resolve the resulting errors though casts or explicit type arguments.
Ideally we would separately track inferences for mutable vs. read-only locations and produce union types whenever it is sound. For example:
Some have made the argument that an error is preferable to a union type, even when a union type would be sound, because
Tis intended to represent a single type. For example, the error onfoo1is better than a union type because theaandbparameters are supposed to be of the same type. I’m not sure I buy that argument when a union type could soundly be inferred.@RyanCavanaugh understood, thanks for the examples, going in your direction of being explicit on the behaviour wanted by the programmer a way to get the best of both worlds might be to add some sort of variance annotations on the generics, thinking of something of the sort:
where if
+is specified the types would mix with aunionand if-is specified types would mix withintersection(+/-is the notation in Scala)Well ideally when we can infer something sound it seems to be a better approach to infer the best candidate instead of forcing an error that will lead to manual casting of the types to a common ancestor