Description
π 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
π» 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.