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

Created on 5 Jun 2017  路  11Comments  路  Source: microsoft/TypeScript

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?

Duplicate

Most helpful comment

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.

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

All 11 comments

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

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.

@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.

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

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.

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

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.

Should be covered by #5579

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)?

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 https://github.com/Microsoft/TypeScript/pull/15473. so do not think we need another issue for it.

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

Was this page helpful?
0 / 5 - 0 ratings