-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inconsistent Type Inference of Object Property In Union Type #48500
Comments
The current rule is (consistently, I guess?) that a potential discriminant property has to have at least one literal type in it to be a discriminant. Using non-literals as discriminants is very problematic because once there are multiple discriminants in play (which would be quite common in practice if this weren't the rule), TS would be arbitrarily selecting which properties to complain about in error messages, and you'd end up with examples like type Message = { kind: "text"; payload: string } | { kind: "money"; payload: number };
declare function sendMessage(m: Message): void
sendMessage(kind: "text", payload: 0); where the problem is "clearly" that |
I still don't quite understand why a field of type Can't there be multiple discriminants in play already, e.g. in type Message = { kind: "text"; payload: "a" } | { kind: "money"; payload: "b" };
declare function sendMessage(m: Message): void
sendMessage({ kind: "text", payload: "b" });
sendMessage({ kind: "money", payload: "a" }); lines 3 and 4 complain about different properties (Playground Link). Is the problem that this would just be more prevalent if it were with any property? |
Whether or not a property is a potential discriminant is about the target type, not the source type. The rule for providing the contextual type on the function is basically:
The bug here, if there is one, is that we really shouldn't consider template string types with holes in them to be discriminants since, unlike other types of literals, the same value can inhabit multiple literal types at the same time: type Closure<T, U> = { arg: T, fn: (_: U) => void };
// Value actually fulfills both branches
const m: `x${string}` = "xz";
const a: Closure<`x${string}`, string> | Closure<`${string}z`, number> = {
arg: m,
fn: (x) => {
// x is string here but we can't correctly disavow number
}
}; but I don't presume that you want this issue "fixed" by adding more implicit |
This is all sort of complicated, though, since you definitely can write string literal types that are visibly disjoint (e.g. |
Agreed, an ideal solution (for this use case at least) would be fewer explicit Your explanation of how discriminant properties work was very helpful in understanding how it is consistent that I think I'm still a little unsure of how/why this is problematic:
given that it seems like this happens (to some degree) already. |
If someone has multiple discriminants and complains that an error message isn't the one they wanted, we can legitimately tell them that it's just sort of Too Bad since any error message in that situation is "equally correct" (this has happened). There are performance implications if any co-present property can be a discriminant, since we will go and try to do all the relational work to make the correct contextual type get selected. Literal checking is very cheap, but if we have to relate arbitrary types to arbitrary types just to find a discriminant that will very often end up not doing anything, it's going to be a big performance problem. The current rule is consistent and useful; I don't think there's a net improvement to be found here. |
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: 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 From your last comment I think that actually, it IS more difficult/less performant to select the contextual type in the |
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. |
Bug Report
🔎 Search Terms
parameter 'x' implicitly has type 'any'
type inference
discriminated union
discriminant property
type narrowing of parent object
🕗 Version & Regression Information
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
Inference of the parameter to the function field
fn
is inconsistent across the two objects. The only difference is the typeT
inClosure<T> | Closure<number>
for the object type:T
isboolean
,"a" | "b"
,null
or even`x${string}`
, the type ofx
asnumber
is correctly inferred whenarg
is set to0
.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 #30506 (comment) is the closest explanation I can find. Maybe type inference works when
T
isboolean ~ true | false
and"a" | "b"
becausearg
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 typestring
could not be a discriminant property if one of type`x${string}`
can be🙂 Expected behavior
Type inference of the parameter
x
to propertyfn
is consistent across typesT
inClosure<T> | Closure<number>
whenarg
is set to0
and0
is not an instantiation ofT
OR
If type inference is not meant to be consistent due to a design limitation (e.g. as in #33205 (comment)), 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
The text was updated successfully, but these errors were encountered: