Repository navigation
Structural never type breaks conditional typesΒ #57210
Copy link
Copy link
Closed as not planned
Labels
Not a DefectThis behavior is one of several equally-correct optionsThis behavior is one of several equally-correct options
Description
π Search Terms
never, intersection, contradiction
π Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about
never
β― Playground Link
π» Code
// These variations on never are both reported by intellisense as `never`.
// That's a problem because they behave differently in a subtle way
type NominalNever = never
// ^?
type StructuralNever = {x:1} & {x:2}
// ^?
// We will be exploring this with the builtin ReturnType generic type
// type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// ReturnType constrains the generic so non-callable types give a useful error message
// @ts-expect-error
type R0 = ReturnType<{x:1}> // error - very helpful!
// `never` is treated specially by the conditional type construct
// the conditional is short-circuited and the conditional evaluates to `never` instead of either the true or false branch.
type R1 = ReturnType<never> // never
// ^?
// but with an impossible intersection,
// 1. the type constraint does not kick in as a safeguard
// 2. the conditional is not short-circuited to 'never'
// 3. the conditional evaluates to the true branch only even though there's no reason to prefer either branch
// the return type is inferred as `unknown`, seemingly because it's an upper bound on the type parameter
// type R2 = unknown; expected never
type R2 = ReturnType<{x:1} & {x:2}>
// ^?
// even if a return type is *structurally* declared, it is ignored by type inference
// type R3 = unknown; expected "X"
type R3 = ReturnType<{x:1} & {x:2} & ((...args: any[]) => "X")>
// ^?π Actual behavior
When intersecting two object types with a conflicting value, the user-observable hinted type is never, but the type interacts with generic templates in a nonsensical way.
π Expected behavior
I expect either (or both):
- conflicting intersections to treated the same as the nominal
nevertype for constrained generics and conditional types. In particular:- The generic constraint would need to be (re)checked, so that an error is reported if the type constraint is refuted.
- The conditional type will need to be (re)checked, so that the conditional is
never
- conflicting intersections to be represented by their structural properties instead of an opaque "
never" type.- This might be nice to do anyway. Such
nevers merit different compiler behavior (e.g. when trying to access a property on a conflicted type) so showing it as e.g.never<"conflicted .x">would go a long way to making the behavior less frustratingly opaque.
- This might be nice to do anyway. Such
Additional information about the issue
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Not a DefectThis behavior is one of several equally-correct optionsThis behavior is one of several equally-correct options
Activity
rotu commentedon Jan 29, 2024
Related to #53027, which observes that impossible intersections behave differently from
neverand attributes the change to #36696Very similar in nature (possibly a re-appearance of the same bug?) to #42859 and looks like it should have been fixed by #42917.
Also, even though a the literal
nevertypes absorbs all non-anytypes under intersection, that is not the case for anevercreated by object intersection. You can "escape" the collapse toneverwith anever-typed property (though note anany-typed property does not do this for some some reason)Workbench Repro
fatcerberus commentedon Jan 29, 2024
This part, at least, seems to have been fully intentional per the linked #36696:
rotu commentedon Jan 29, 2024
Yeah, definitely intentional, but also strange. The rationale given was that if a property had disjoint types, the object type can't exist. But the
nevertype is disjoint from all types by definition!RyanCavanaugh commentedon Feb 1, 2024
It's not possible to statically detect all "structural
nevers" -- the conflicting property can be arbitrarily deep and may involve unbounded computation. We can't remove this problem, only make it more difficult to understand by making it more inconsistent as to when it appears.fatcerberus commentedon Feb 1, 2024
Ryan Cavanaugh (@RyanCavanaugh) This issue is about intersections that are already statically reduced to
neverbehaving differently fromnever, though.rotu commentedon Feb 1, 2024
I'm not saying we should detect all structural
nevers.{x:{y:number}} & {x:{y:string}}is not reduced toneverand that's okay because, in type expressions, it behaves structurally. For example,ReturnTyperejects it as having no call signature.The case
type R2 = ReturnType<{x:1} & {x:2}>above is especially troubling. Either it should beneverbecause of the set-of-values interpretation or it should be an error under a structure-of-values interpretation. In neither case should it be the current result,unknown.π
Previously
type IsNever<X> = X extends never ? true : falsehad two possible outcomes:falseornever. Now it has threefalse,never, ortrue.typescript-bot commentedon Feb 4, 2024
This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
rotu commentedon Feb 4, 2024
Ryan Cavanaugh (@RyanCavanaugh) please reopen. I believe you missed the thrust of this issue.