You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Impossible intersection is supposedly never but any can still be assigned to it #53027
This is the behavior in every version I tried (including nightly), and I reviewed the FAQ for relevant entries. But really this is a loose end left by #36696; prior to that the impossible intersection (AB below) was not never at all.
typeA={kind: 'a',foo: string};typeB={kind: 'b',foo: number};typeAB=A&B;// never, supposedly// @ts-expect-error: Type 'any' is not assignable to type 'never'.constn1: never={}asany// but this is okconstn2: AB={}asany
🙁 Actual behavior
any is assignable to AB, despite the fact that any is not assignable to never and AB is supposedly never (per both #36696 and hovering it)
Two problems I know of that seem to be caused by this.
First, it means assigning to the quasi-never type AB gives errors on each field rather than on the object as a whole, which makes those errors harder to understand IMO:
constn3: AB={// @ts-expect-error: Type 'number' is not assignable to type 'never'.a: 3,}
Second, it means that Omit<AB, K> gives an incorrect type (even when using the conditional type trick such that it does work with literal never:
typeDistributedOmit<T,Kextendskeyofany>=Textendsany ? Omit<T,K> : never// @ts-expect-error: Type '{}' is not assignable to type 'never'.constn4: DistributedOmit<never,"bogus">={}// allowedconstn5: DistributedOmit<AB,"bogus">={}
(To be clear, these are all simplified from something I ran into in real code.)
Sure, I guess that was intended as the simplest example of "this type is supposedly never but is not in fact equivalent to never". Apologies if I misunderstood how these are related; if it's more useful, I'm happy to make a new issue framed around those examples.
Yeah, the exact manifestation / what you care about matters. These are all orthogonally addressable, in theory.
The PR notes that this is a consequence of the reduction being deferred:
In #31838 we reduce empty intersections to never immediately upon construction. However, in this PR we have to defer the determination because it requires us to resolve the types of properties in the constituent object types. Specifically, when two or more constituent object types have properties with the same name, we need to resolve the types of those properties in order to determine if they're disjoint discriminants, but doing so during intersection construction can easily cause circularities. So, instead of reducing intersections upon construction, we reduce them immediately before accessing members, relating them to other types, or converting them to their string representations in diagnostics and quick info.
I'm not sure if there are any cures on the table which are better than the disease here. Basically, not all of these can be true at once:
Nothing (except never) is assignable to never
S is assignable to T & U if S is assignable to T and S is assignable to U
any is assignable to anything that isn't never
Just mentioning the type A & B won't introduce new circularity errors into your code
so we have to park the inconsistency somewhere. It's extremely rare to see a zero-order intersection that reduces to never in practice, so choosing that scenario as the one to be the most consistent at the expense of violating other invariants seems like a bad choice. I'd be curious to know more about how you ended up passing around an unreduced intersection that was actually never; these typically only happen in higher-order scenarios where they get reduced out of the final union before they can land in usercode and cause problems.
IMO probably the most convincing example that something is wrong is something like this:
// x is effectively { [s: string]: never }typeX=Omit<A&B,"nada">;constx: X={};// Allowed initialization, maaaaybe ok per definition of X?consts: string=x.anything;// Allowed property access, sadconsole.log(s.toLowerCase());// Crash, sad.
but the initialization of X only succeeds if the initializer is exactly { }; anything else would fail, same as here
// @ts-expect-error: Type '{}' is not assignable to type 'never'.constn4: DistributedOmit<never,"bogus">={}
I'm not sure if there are any cures on the table which are better than the disease here. Basically, not all of these can be true at once:
Nothing (except never) is assignable to never
S is assignable to T & U if S is assignable to T and S is assignable to U
any is assignable to anything that isn't never
Just mentioning the type A & B won't introduce new circularity errors into your code
Got it -- I don't know that I understand all the details of the implementation issues here but I see why it's useful for you to know which inconsistencies are the most relevant. In the case where this came up for me, I think the most confusing thing is that the errors are on the fields rather than the whole object? Definitely the fact that any is assignable to this type is the least relevant to me.
I'd be curious to know more about how you ended up passing around an unreduced intersection that was actually never; these typically only happen in higher-order scenarios where they get reduced out of the final union before they can land in usercode and cause problems.
The full code at issue is quite complicated, but let me see if I can summarize without eliding anything that might be relevant.
The idea is basically we have some types which represent our database, and many of those have a field parentTable which is some union of other database tables and parentId which is then the ID of some row of one of those tables. Also, some of those types are discriminated unions of further subtypes. We generate a bunch of test data factories which accept basically a subset of the database fields, and fill in the rest with defaults (so basically they accept Omit<T, ...> and return T).
Then we generate further factories which, given some such database value, generate valid children of that database value, automatically setting parent*. So those want to accept only those subtypes of the type we are generating that have the right parent table. The impossible intersection arises when you try to call one with an invalid subtype; the arguments are the type similar to Omit<AB, "kind">.
Activity
RyanCavanaugh commentedon Feb 28, 2023
Is this behavior observable in a way that matters?
benjaminjkraft commentedon Feb 28, 2023
Two problems I know of that seem to be caused by this.
First, it means assigning to the quasi-never type
ABgives errors on each field rather than on the object as a whole, which makes those errors harder to understand IMO:Second, it means that
Omit<AB, K>gives an incorrect type (even when using the conditional type trick such that it does work with literalnever:(To be clear, these are all simplified from something I ran into in real code.)
Full playground here.
RyanCavanaugh commentedon Feb 28, 2023
The presented issue was that you don't want
anyto be assignable toAB, though, which seems totally unrelated.benjaminjkraft commentedon Feb 28, 2023
Sure, I guess that was intended as the simplest example of "this type is supposedly never but is not in fact equivalent to never". Apologies if I misunderstood how these are related; if it's more useful, I'm happy to make a new issue framed around those examples.
RyanCavanaugh commentedon Mar 3, 2023
Yeah, the exact manifestation / what you care about matters. These are all orthogonally addressable, in theory.
The PR notes that this is a consequence of the reduction being deferred:
I'm not sure if there are any cures on the table which are better than the disease here. Basically, not all of these can be true at once:
never) is assignable toneverSis assignable toT & UifSis assignable toTandSis assignable toUanyis assignable to anything that isn'tneverA & Bwon't introduce new circularity errors into your codeso we have to park the inconsistency somewhere. It's extremely rare to see a zero-order intersection that reduces to
neverin practice, so choosing that scenario as the one to be the most consistent at the expense of violating other invariants seems like a bad choice. I'd be curious to know more about how you ended up passing around an unreduced intersection that was actuallynever; these typically only happen in higher-order scenarios where they get reduced out of the final union before they can land in usercode and cause problems.IMO probably the most convincing example that something is wrong is something like this:
but the initialization of
Xonly succeeds if the initializer is exactly{ }; anything else would fail, same as herewhich is sort of a niche problem.
benjaminjkraft commentedon Mar 8, 2023
Got it -- I don't know that I understand all the details of the implementation issues here but I see why it's useful for you to know which inconsistencies are the most relevant. In the case where this came up for me, I think the most confusing thing is that the errors are on the fields rather than the whole object? Definitely the fact that
anyis assignable to this type is the least relevant to me.The full code at issue is quite complicated, but let me see if I can summarize without eliding anything that might be relevant.
The idea is basically we have some types which represent our database, and many of those have a field
parentTablewhich is some union of other database tables andparentIdwhich is then the ID of some row of one of those tables. Also, some of those types are discriminated unions of further subtypes. We generate a bunch of test data factories which accept basically a subset of the database fields, and fill in the rest with defaults (so basically they acceptOmit<T, ...>and returnT).Then we generate further factories which, given some such database value, generate valid children of that database value, automatically setting
parent*. So those want to accept only those subtypes of the type we are generating that have the right parent table. The impossible intersection arises when you try to call one with an invalid subtype; the arguments are the type similar toOmit<AB, "kind">.Here's all of that in playground.