Skip to content

Suggestion: Allow use of const enum keys as keys in interfaces or type aliases #16258

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

Closed
lostfictions opened this issue Jun 5, 2017 · 11 comments
Labels
Duplicate An existing issue was already created

Comments

@lostfictions
Copy link

I'm currently writing a small library to make working with the Web MIDI API slightly nicer, and I'd like to write event handling code that correctly types the event handler signature (similar to how eg. lib.dom.d.ts types Document.addEventListener generically with keys/values from the DocumentEventMap interface.)

MIDI events can be dispatched frequently and it's often desirable to handle them with minimum latency, so I'm using const enums to provide an abstraction over MIDI event data (passed in a UInt8Array by the Web MIDI API and decoded with bit shifts and masks) without the additional overhead of object lookups.

Since const enums are fully known at design time, it seems like it should be possible to use their keys as keys in interface declarations like lib.dom.d.ts does it:

const enum ChannelMessage {
  'noteoff' = 0x8,
  'noteon' = 0x9,
  'keyaftertouch' = 0xA,
  // ...
}

interface ChannelMessageEventMap {
  [ChannelMessage.noteoff] : NoteEvent
  [ChannelMessage.noteon] : NoteEvent
  [ChannelMessage.keyaftertouch] : AftertouchEvent
  // ...
}

addEventListener<K extends keyof ChannelMessageEventMap>(type: K, listener: (ev: ChannelMessageEventMap[K]) => any): void

Unfortunately, this doesn't currently work -- const enum keys can't be used in interfaces. I thought in a pinch I could simply use the enum values as keys in the event map interface instead, that is:

interface ChannelMessageEventMap {
  0x8 : NoteEvent
  0x9 : NoteEvent
  0xA : AftertouchEvent
  // ...
}

The above compiles -- but the following usage of addEventListener won't (assuming the signature declared above):

addEventListener(ChannelMessage.noteoff, ev => { /* ... */ })
//               ^^^^^^^^^^^^^^^^^^^^^^
// ERROR: Argument of type 'ChannelMessage.'noteoff'' is not assignable to parameter of type '"8" | "9" | "10" | ...

Even though const enum values are fully known and inlined at compile time, it seems they aren't compatible with the value literal types themselves.

It's not clear to me if there's any other way to make this work with const enums, unfortunately -- their usefulness as compile-time constants seems greatly reduced by their incompatibility and the inability to use them in other places you'd be able to use a constant value or a type literal. I guess I maybe have to bite the bullet and map to string keys after all?

@RyanCavanaugh
Copy link
Member

Similar to #5579 but it might be more tractable to do this only for keys which originate in enums

@jsen-
Copy link

jsen- commented Jun 8, 2017

I'm running into similar issue, trying to create function translating HTTP status codes to messages.
Something like (playground)

const STATUS_CODES = {
    404: "Not found",
    500: "Internal server error",
};

type NumericKeys<T extends { [key: number]: string }> = {
    [P in keyof T]: string;
};

function code2msg(code: keyof NumericKeys<typeof STATUS_CODES>) {
    return STATUS_CODES[code];
}

code2msg("404"); // works
code2msg(404); // fails

It fails due to keyof NumericKeys<typeof STATUS_CODES> is of type "404" | "500" instead of 404 | 500.
Which in turn is due to keyof being always of type string even when the constraint of NumericKeys<T extends { [key: number]: string }> is used.

Would it be possible for keyof to be constrained by it's outer generic type (instead of always string)?
Motivation is to encode in the type system that code2msg can only be called on supported status codes.

@RyanCavanaugh
Copy link
Member

@jsen- I think the behavior there is really what you want (or at least should want, if that's a thing). Consider if you had written something like this:

function code2msg(code: keyof NumericKeys<typeof STATUS_CODES>) {
    for (var k of STATUS_CODES) {
      if (k === code) return STATUS_CODES[k];
    }
    return undefined;
}

which looks like it should do the same thing, but doesn't. Since we don't really know what you're going to use a keyof for, the only safe thing is to always say string.

@jsen-
Copy link

jsen- commented Jun 8, 2017

I believe you meant

for (var k in STATUS_CODES) {

Under my logic, k === code should be a compile time error comparing k: string with code: 404 | 500

@Jessidhia
Copy link

This affects mapped types in more natural use cases as well.

const enum Foo {
  Bar = 'bar'
}

type FooMap = {[key in Foo]: boolean}

const foo: FooMap = {
  [Foo.Bar]: true
}
Type '{ [x: string]: boolean; }' is not assignable to type 'FooMap'.
Property 'bar' is missing in type '{ [x: string]: boolean; }'.

I also seem to not be able to index the mapped type but I can't reproduce it in a small example; I assume it only happens if the enum is imported from somewhere else.

const isBar = foo[Foo.Bar] // not an error in this example, but errors out with missing index signature in a more complex case

@michaeljota
Copy link

Actually it works as expect when compiled, it's just the type checking that broke and get lost. Tested in Playground with number enums only, but it should also work with string enums if the compiled object it's the same one.

@mhegazy mhegazy added Bug A bug in TypeScript Duplicate An existing issue was already created and removed Bug A bug in TypeScript labels Aug 21, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Aug 21, 2017

Should be covered by #5579

@begincalendar
Copy link
Contributor

Is it possible to not conflate this issue with #5579?

I get that they are related, but given that #5579 has been open for nearly two years and suffers from complexity such as mentioned by Ryan here, is it possible to assess the feasibility of making this happen for constant expressions (at least for now)?

@sanex3339
Copy link

sanex3339 commented Sep 1, 2017

@mhegazy
Copy link
Contributor

mhegazy commented Sep 5, 2017

I get that they are related, but given that #5579 has been open for nearly two years and suffers from complexity such as mentioned by Ryan here, is it possible to assess the feasibility of making this happen for constant expressions (at least for now)?

we have a fix for #5579 in #15473. so do not think we need another issue for it.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 20, 2017

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@mhegazy mhegazy closed this as completed Sep 20, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

8 participants