Skip to content
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

Index signature error for mapped type when keyed on declare enum #31771

Closed
thw0rted opened this issue Jun 5, 2019 · 4 comments · Fixed by #31784
Closed

Index signature error for mapped type when keyed on declare enum #31771

thw0rted opened this issue Jun 5, 2019 · 4 comments · Fixed by #31784
Assignees
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@thw0rted
Copy link

thw0rted commented Jun 5, 2019

TypeScript Version: 3.2.4 plus whatever's on the Playground

Search Terms: enum mapped index signature

Code

enum E1 { ONE, TWO, THREE }
declare enum E2 { ONE, TWO, THREE }

type Bins1 = { [k in E1]?: string; }
type Bins2 = { [k in E2]?: string; }

const b1: Bins1 = {};
const b2: Bins2 = {};

const e1: E1 = E1.ONE;
const e2: E2 = E2.ONE;

// These are fine
b1[1] = "a";
b1[e1] = "b";

// Fails (correctly) because we can't know that `1` is a valid index at compile time
b2[1] = "a";
// Gives "Element implicitly has an 'any' type because type 'Bins2' has no index signature." but shouldn't
b2[e2] = "b";

Expected behavior: Indexed access to a mapped type should be possible as long as we guarantee ahead of time that the index value is of the correct key type.

Actual behavior: Compiler allows indexed access only if it knows that the enum value is a literal.

Playground Link: Here, make sure to turn on noImplicitAny

Related Issues: #13042 appears to work but only addresses locally-defined enums

I'm trying to key on enum-values from a library I depend on so I'm stuck using ambient declarations. If this is "by design", or allowing enum values whose runtime contents cannot be determined at compile time is too difficult to support, I'd welcome suggestions for an alternate approach.

@thw0rted
Copy link
Author

thw0rted commented Jun 5, 2019

Just to be clear, in my actual use case, the enum is declared in a .d.ts file as

declare module "mylib" {
  enum E2 { ONE, TWO, THREE }
}

In my code, I import { E2 } from "mylib". As far as I know, this is functionally equivalent to the declare enum I gave above. This is based on my reading of @RyanCavanaugh 's answer on SO about types of enums, but I could have misunderstood something.

@ahejlsberg ahejlsberg added the Suggestion An idea for TypeScript label Jun 5, 2019
@ahejlsberg
Copy link
Member

This is technically working as intended, but a bit convoluted. In an enum declaration in an ambient context (i.e. with a declare modifier or in a .d.ts file), members without initializers are considered computed members with unknown values. This contrasts with a non-ambient context where members without initializers are given auto-incremented numeric literal values. For this reason, E2 above is considered a numeric enum type and not a literal enum type (see #15486 for the distinction).

Now, the thing that maybe isn't right here is the behavior of a numeric enum type as the key type in a mapped type, such as Bins2 above. Since E2 is really just a number type with a different name, it would be reasonable to include a numeric index signature in Bins2, but we don't. I'm tagging this issue as a suggestion to that effect.

@thw0rted
Copy link
Author

thw0rted commented Jun 6, 2019

I think I'm learning that ambient enum declarations are pretty weird. After your comment, I went and poked around in the playground and found a couple of edge cases that also seem "a bit convoluted".

declare enum E1 { ONE, TWO, THREE = true } // "In ambient enum declarations member initializer must be constant expression." (isn't `true` constant?)
declare enum E2 { ONE, TWO, THREE = 'x' }

type B2 = { [k in E2]?: string; } // Type 'E2' is not assignable to type 'string | number | symbol'.

let e2: E2 = E2.ONE;
e2 = E2.THREE; // no error
e2 = 'x'; // Type '"x"' is not assignable to type 'E2'.

I was surprised to find that you can include value assignments in the ambient declaration in the first place, because I can't think of another example where the description of the shape of a module includes information about runtime values -- it seems like an intrusion on the type space.

Is there a different syntax to use to say that this module exports an enum-like object, and I don't know or care about the type of its values? I guess you'd have to at least be able to say that they're constrained to string | number | Symbol if you're going to use them in a mapped type / index signature, so the easy assumption is that unless I say otherwise, they'll be numbers.

@ahejlsberg
Copy link
Member

I agree the first error in your example could have a better error message. true is indeed a constant, but it is not assignable to string or number as is required for an enum member.

The second error is fixed by #31784.

The third error is intended. String literal types are not considered assignable to string valued enum literal types (but the reverse is allowed).

I think I'm learning that ambient enum declarations are pretty weird.

Yeah. The weirdness mostly comes from the fact that, for reasons of backward compatibility, we have two subtly different flavors of enums, as described in #15486. In order for E1 and E2 to behave identically in your original example you need to write:

enum E1 { ONE, TWO, THREE }
declare enum E2 { ONE = 0, TWO = 1, THREE = 2 }

The subtlety here is that the absence of an initializer generally implies auto-numbering, except in an ambient non-const enum where it implies a computed value.

I was surprised to find that you can include value assignments in the ambient declaration in the first place

Except for the odd case of computed enum members (which, again, are there mostly for backwards compatibility reasons), enum members are just labelled literal constants and the compiler definitely needs to know their values in order to use them for control flow analysis, etc. This holds whether or not the enums are declared ambiently.

Is there a different syntax to use to say that this module exports an enum-like object, and I don't know or care about the type of its values?

You could just export an object literal expression.

@ahejlsberg ahejlsberg added the Fixed A PR has been merged for this issue label Jun 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants