Skip to content

Element type of enum should include enum typeΒ #55713

Closed as not planned
Closed as not planned
@cpcallen

Description

@cpcallen

πŸ”Ž Search Terms

enum element type computed index reverse lookup

πŸ•— Version & Regression Information

  • This is the behaviour in every version I tried, and I reviewed the FAQ for entries about enums

⏯ Playground Link

https://www.typescriptlang.org/play?#code/KYOwrgtgBAolDeBBAvgbgFDoGYHsBOUAFAMY4gDOALlANbACeUAliLAJQLpTdQA2w1APqCoAXlgBtOvQC6GHlAD0i7gD0A-Fx6kKOfgDpeOAOaFK9AA7AcWKMLYZkQA

πŸ’» Code

enum E {A};

for (const key in E) {
    let __ = E[key];
    //  ^?
    console.log(typeof __);
}

πŸ™ Actual behaviour

The (tsc-reported) type of __ is string

πŸ™‚ Expected behaviour

The (tsc-reported) type of __ should be 'A' | E, or at least string | number`.

Additional information about the issue

This issue singles out one of several different problems discussed in #39627.

See also #39627 and #42457, which discuss iterating over enum entries in different contexts.

Background

We are migrating a (Closure type system) JavaScript library to TypeScript.

In times past our code looked like:

/** @class */
function C() { /* ... */ }

C.prototype.E_A = 0;
C.prototype.E_B = 1;

In preparation for the migration to TypeScript, and since the E_x properties are used as an enum, we first converted them to an @enum but included code to maintain backwards compatibility as follows:

/** @enum */
const E = {
    A: 0,
    B: 1,
};

class C{
  constructor () {
    for (const key in E) {
       this['E_' + key] = E[key];
    };
  }
}

My initial naΓ―ve conversion to TypeScript:

enum E {
    A,
    B,
};

class C {
  constructor () {
    // Copy enum values onto this for backwards-compatibility:
    for (const key in E) {
       this['E_' + key] = E[key];
    };
  }
}

helpfully elicited the (correct and useful) error "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'C'. No index signature with a parameter of type 'string' was found on type 'C'."

Since this is only for backwards compatibility with legacy client code I don't wish to add an index signature to type C (new client code written in TypeScript should use the enum directly), so instead I attempted to simply coerce the type of this to a allow the assignments:

    for (const key in E) {
       (this as unknown as Record<string, number>)['E_' + key] = E[key];
    };

This produces the error message "Type 'string' is not assignable to type 'number'.(2322)", which is useful, insofar as it (eventually) alerted me tho the change of semantics of E, which now also includes the reverse-lookup table, but it is incorrect because the actual type of E[key] should be something like (keyof typeof E) | E, or at least string | E (or failing that even just string | number)β€”but definitely not just string.

This inspecificity of the type of an enum when using it for reverse lookups has previously been the subject of issues #38806 and #50933, but those issues do not highlight the fact that the type only supports reverse lookups.

Surprisingly, adding a guard clause is not sufficient to satisfy the type checker:

    for (const key in E) {
       if (typeof E[key] === 'string') continue;
       (this as unknown as Record<string, number>)['E_' + key] = E[key];
    };

This still complains that E[key] is a string even though string has been explicitly eliminated. [Update: this turns out to be due to #10530β€”or rather any of its many supposed duplicates, since the specific original example reported there has been fixed.]

Adding a temporary variable however does work:

    for (const key in E) {
       const value = E[key];
       if (typeof value === 'string') continue;
       (this as unknown as Record<string, number>)['E_' + key] = value;
    };

because tsc 'correctly' deduces that value has type never. It is not clear why it does not make the same deduction about E[key] in the previous snippet.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions