Skip to content

Indexed access lookup on enum type could produce specific key types #50933

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
jfw225 opened this issue Sep 23, 2022 · 12 comments
Open

Indexed access lookup on enum type could produce specific key types #50933

jfw225 opened this issue Sep 23, 2022 · 12 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@jfw225
Copy link

jfw225 commented Sep 23, 2022

Suggestion

Per the documentation, it is possible to reverse map an enum at runtime:

enum Enum {
  A,
}
 
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

However, suppose we wanted to get the type of the enum name from the enum value at compile time. That is, get some type T = "A" from Enum.A. From the example above, we would expect something like the following to work:

type T = typeof Enum[Enum.a]; // evaluates to string rather than "A"

const A: T = "A"; // compiler has no issues
const B: T = "B"; // compiler has no issues

As written above, the compiler fails to get the correct type. However, I have found a workaround:

type T = keyof { [K in keyof typeof Enum as typeof Enum[K] extends Enum.A ? K : never]: any }; // evaluates to "A"

const A: T = "A"; // compiler has no issues
const B: T = "B"; // compiler now throws a fit as expected

It would be nice if this could be abstracted away from the developer.

@fatcerberus
Copy link

type T = Enum[Enum.A]?

@jfw225
Copy link
Author

jfw225 commented Sep 24, 2022

image

Same goes if you try

type EnumKey = keyof typeof Enum // "A"
type EnumValWrong = Enum[EnumKey] //  Property "A" does not exist on type "Number"
type EnumVal = typeof Enum[EnumKey] // Enum.A

Thus, you need the typeof in front of what you wrote:

// type T = Enum[Enum.A];
type T = typeof Enum[Enum.A];

But as I said above, this evaluates to string rather than "A".

@jcalz
Copy link
Contributor

jcalz commented Sep 24, 2022

Isn’t this a duplicate of #38806?

@jfw225
Copy link
Author

jfw225 commented Sep 24, 2022

Isn’t this a duplicate of #38806?

Potentially. I read that post as more of an issue reporting that what you would expect to work did not work for that specific case. I opened this to offer a more general suggestion for the problem rather than report an issue since the problem mentioned in #38806 can be solved with the workaround I mentioned above.

My apologies if this is a duplicate--this is my first issue on the repo. I did find #38806 before I posted this and linked this thread afterward as a solution to their problem.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Declined The issue was declined as something which matches the TypeScript vision labels Sep 26, 2022
@RyanCavanaugh
Copy link
Member

You're basically asking for a type alias to do a thing you can already today in userland, which isn't something we do.

@RyanCavanaugh
Copy link
Member

A simpler form is

type KeyHelper<E, K extends keyof E, V> = K extends unknown ? E[K] extends V ? K : never : never;
type KeyName<E, V> = KeyHelper<E, keyof E, V>;
type TheKeyName = KeyName<typeof Enum, Enum.B>;

@jfw225
Copy link
Author

jfw225 commented Sep 27, 2022

Thank you for providing a simpler form of the type alias. I appreciate the thought that went into your reply. Maybe you can help me better understand my underlying confusion.

You're basically asking for a type alias to do a thing you can already today in userland, which isn't something we do.

Okay, that makes sense. However, I feel like T should evaluate to "A" rather than string in the following example. Wouldn't this be the expected functionality given the reverse-mapping showcased in the documentation?

const nameOfA = Enum[Enum.A]; // evaluates to "A"
type T = typeof Enum[Enum.A]; // evaluates to string

It's entirely possible (probably likely) that this functionality is intended and there is something that I am missing. I would be interested to hear your thoughts on the advantages of the current implementation.

@RyanCavanaugh
Copy link
Member

No reason it couldn't be the more-specific type "A"; that's just a feature that no one has implemented (or even proposed) as of yet.

@fatcerberus
Copy link

fatcerberus commented Sep 28, 2022

@RyanCavanaugh I kind of get the impression this issue counts as that proposal (and you tagged it Declined 😉).

From the example above, we would expect something like the following to work:

type T = typeof Enum[Enum.a]; // evaluates to string rather than "A"

const A: T = "A"; // compiler has no issues
const B: T = "B"; // compiler has no issues

@RyanCavanaugh
Copy link
Member

Well, fair 😅

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed Declined The issue was declined as something which matches the TypeScript vision labels Sep 28, 2022
@RyanCavanaugh RyanCavanaugh changed the title Enum Reverse Type Mappings Indexed access lookup on enum type could produce specific key types Sep 28, 2022
@GauBen
Copy link

GauBen commented Mar 11, 2024

Hey there, trying to have this issue on the backlog by adding my use case:

https://www.typescriptlang.org/play?#code/KYOwrgtgBAolDeAoKKoDECWAnAzgFwBplUBlYAYwHsQATRAX0UQDMwRy8NqoBzYPAHIBDCMAAUADwBcsAJQJiKLPzBYQsANoSAugyYAbflAlQAvL37DRYmADpMuPPJxDOOZhmA4oAa2ABPSmYoPH8AB2Ag2ChkAHpYqAA9AH4gA

enum E {
    First,
    Second
}

function getName(x: E) {
    return E[x]
}

let x = getName(E.First) satisfies keyof typeof E 
 // ^? string

The exact use case is retrieving names from protobuf enums, which are implemented as numeric enums:

/** @generated */
export enum Something {
  /** @generated */
  UNSPECIFIED = 0,
  /** @generated */
  INTERNAL = 1,
  // ...
}

We send integers over the wire, but return strings in APIs. We would like that Something[x as Something] to be keyof typeof Something rather than string

Edit: and we also use Something[Something.INTERNAL | Something.XYZ] and we would like it to be typed "INTERNAL" | "XYZ"

@jiangzm
Copy link

jiangzm commented Jun 27, 2024

image

Same goes if you try

type EnumKey = keyof typeof Enum // "A"
type EnumValWrong = Enum[EnumKey] //  Property "A" does not exist on type "Number"
type EnumVal = typeof Enum[EnumKey] // Enum.A

Thus, you need the typeof in front of what you wrote:

// type T = Enum[Enum.A];
type T = typeof Enum[Enum.A];

But as I said above, this evaluates to string rather than "A".

Add a reversal type

enum Enum {
  A,
  B,
}

type ToNumber<S> = S extends `${infer N extends number}` ? N : S;

type EnumKey = keyof typeof Enum; // "A" | "B"
type EnumVal = ToNumber<`${Enum}`>; // 0 | 1

type EnumForward = {
  [key in EnumKey]: (typeof Enum)[key];
}; // { A: Enum.A; B: Enum.B; }

type EnumReverse = {
  [key in EnumKey as (typeof Enum)[key]]: key;
}; // { 0: "A"; 1: "B"; }

type EnumAKey = EnumReverse[Enum.A]; // "A"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants