Skip to content

Generic type sometimes returns never instead of actual template parameter type #41778

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

Open
nin-jin opened this issue Dec 2, 2020 · 6 comments
Labels
Bug A bug in TypeScript Experimentation Needed Someone needs to try this out to see what happens Help Wanted You can do this
Milestone

Comments

@nin-jin
Copy link

nin-jin commented Dec 2, 2020

TypeScript Version: 3.6+

Search Terms: enum, extends, never, template parameters

Code

enum FL4 { Absurd, False, True, Unknown }

type Classify< Left, Right > = Left extends Right

	? Right extends Left
		? [ Left , '==', Right ]
		: [ Left , '<:', Right ]
	
	: Right extends Left
		? [ Left, ':>', Right ]
		: [ Left , '!=', Right ]

Actual at 3.6+

type Absurd_is_never_wtf = Assert<
    Classify< FL4.Absurd, 0 >,
    [ never, '<:', 0 ]
>

type One_is_never_wtf = Assert<
    Classify< FL4.Absurd, 1 >,
    [ FL4.Absurd, ':>', never ]
>

Expected and before 3.6

type Absurd_is_Absurd = Assert<
    Classify< FL4.Absurd, 0 >,
    [ FL4.Absurd, '==', 0 ]
>

type One_is_One = Assert<
    Classify< FL4.Absurd, 1 >,
    [ FL4.Absurd, ':>', 1 ]
>
```
@nin-jin nin-jin changed the title Type returns never instead of actial template parameter type Generic type sometimes returns never instead of actial template parameter type Dec 2, 2020
@nin-jin nin-jin changed the title Generic type sometimes returns never instead of actial template parameter type Generic type sometimes returns never instead of actual template parameter type Dec 2, 2020
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Dec 2, 2020
@RyanCavanaugh
Copy link
Member

@ahejlsberg shorter and with distributivity removed

enum E { A, B }

type Classify<Left, Right> = [Left] extends [Right] ? [Right] extends [Left] ? [Left, '==', Right] : [Left, '<:', Right] : [Right] extends [Left] ? [Left, ':>', Right] : [Left, '!=', Right];

// M: [E.A, 1, never]
type M = Classify<E.A, 1>;

I would suspect this is caused by the assignability/subtyping relationship difference for number/enum comparisons -- do we need to consistently use one or the other when applying the narrowings in the conditional type branches?

@ahejlsberg
Copy link
Member

The culprit is this rule we have for the assignable relation (from comment in isSimpleTypeRelatedTo): "Type number or any numeric literal type is assignable to any numeric enum type or any numeric enum literal type. This rule exists for backwards compatibility reasons because bit-flag enum types sometimes look like literal enum types with numeric literal values."

So, we consider 1 assignable to E.A and we then form the intersection 1 & E.A in the true branch--which ends up being never because intersections consider 1 and E.A to be distinct types.

Not sure what we can do here.

@RyanCavanaugh RyanCavanaugh added Experimentation Needed Someone needs to try this out to see what happens and removed Needs Investigation This issue needs a team member to investigate its status. labels Dec 14, 2020
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Dec 14, 2020
@RyanCavanaugh
Copy link
Member

The spitballed solution would be to say that 1 (the literal type) is not assignable to E.A due to the value mismatch. This would create a new transitivity hole (1 -> number, number -> E.A, but not 1 -> E.A) but patches up the intersection/assignability hole (if T -> U, then T & U should not ever be never).

We'd be interested to evaluate a PR for breaks. Presumably this could be a bugfinder -- const m: E.A = 1 should be an error, after all.

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Help Wanted You can do this labels Dec 14, 2020
@nin-jin
Copy link
Author

nin-jin commented Dec 16, 2020

const m: E.A = 1 should be an error

I think no. It will be more usefull if enums are simply union of literals. Nothing more.

@MaximeKjaer
Copy link

We encountered this issue over at socketio/socket.io#3833. Here's another minimized bug reproduction:

enum Enum {
  NAME = "name",
  WRONG_NAME = "wrong name"
}
type Name = "name";
type NameOrNull<N> = N extends Name ? N : null;

// Sanity checks:
type test1 = NameOrNull<"name">; // returns type `"test"`
type test2 = NameOrNull<"wrong name">; // returns type `null`
type test3 = Enum.NAME extends Name ? Enum.NAME : null // returns type `Enum.NAME`
type test4 = NameOrNull<Enum.WRONG_NAME>; // returns type `null`

// Unexpected behavior:
type test5 = NameOrNull<Enum.NAME>; // returns type `never`, but expected type `Enum.NAME`

@jcalz
Copy link
Contributor

jcalz commented May 23, 2022

So then this is a duplicate/related to #21998, I think. Why can't we have the intersection of an enum and the wider literal it comes from just be the enum? Especially for string enums, which doesn't have the bitflag craziness to deal with?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Experimentation Needed Someone needs to try this out to see what happens Help Wanted You can do this
Projects
None yet
Development

No branches or pull requests

5 participants