Fix: Enum keys not accepted as computed properties with non-identifier names
How microsoft/TypeScript#25083 fixed enum keys in computed properties — why computed property names with non-identifier enum values were rejected by the type checker.
The Bug
Repo: microsoft/TypeScript Issue: #25083 Status: closed-not-merged PR: https://github.com/microsoft/TypeScript/pull/63526
In TypeScript, enum members can be used as computed property names in object literals. However, when the enum has members with non-identifier names (e.g., string literal enum members like 'my-key'), the type checker incorrectly rejected them in computed property positions. For example [1]:
enum Keys {
'my-key' = 'my-key',
simple = 'simple',
}
// This should be valid but was a compiler error:
const obj: Record<Keys, number> = {
[Keys['my-key']]: 1, // Error: Type 'string' cannot be used as an index type
[Keys.simple]: 2, // OK
};
The issue was that isLateBindableAST in src/compiler/checker.ts only accepted entity name expressions (identifiers, dotted names) as computed property names. String literal enum members like 'my-key' are not valid identifiers (they contain a hyphen) and were therefore rejected [1].
Root Cause
In src/compiler/checker.ts, the isLateBindableAST function determines whether a declaration name can be “late bound” (resolved during type checking rather than during binding). The original code [2]:
function isLateBindableAST(node: DeclarationName) {
if (!isComputedPropertyName(node) && !isElementAccessExpression(node)) {
return false;
}
const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression;
return isEntityNameExpression(expr);
}
isEntityNameExpression only returns true for identifiers (foo) and dotted names (A.B). String literal expressions like Keys['my-key'] are ElementAccessExpression (not entity name expressions), so they were rejected by the guard. But Keys['my-key'] is a perfectly valid computed property name — the string literal 'my-key' should be accepted as the property key [2].
The Fix
@@ -13785,13 +13785,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
&& isTypeUsableAsIndexSignature(isComputedPropertyName(node) ? checkComputedPropertyName(node) : checkExpressionCached((node as ElementAccessExpression).argumentExpression));
}
- function isLateBindableAST(node: DeclarationName) {
- if (!isComputedPropertyName(node) && !isElementAccessExpression(node)) {
- return false;
- }
- const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression;
- return isEntityNameExpression(expr);
- }
+ function isLateBindableAST(node: DeclarationName) {
+ if (!isComputedPropertyName(node) && !isElementAccessExpression(node)) {
+ return false;
+ }
+ const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression;
+ return isEntityNameExpression(expr) ||
+ isElementAccessExpression(expr) && isStringLiteral(expr.argumentExpression) && isEntityNameExpression(expr.expression);
+ }
The fix adds a clause: if the computed property expression is an element access with a string literal argument on an entity name expression (like Keys['my-key']), it is accepted as late-bindable. This precisely covers the enum-member-as-computed-property case without opening the door to arbitrary dynamic access patterns [2].
Why String Literals Only?
The fix deliberately restricts the new clause to string literals (isStringLiteral(expr.argumentExpression)). Why not allow any element access? [3]
- String literals are statically known — the compiler can resolve
'my-key'at compile time, so late binding is safe. - Entity name on the left —
Keysmust be a known entity (enum, namespace, class). This prevents arbitrary computed access likesomeObj[variable]. - Prevents dynamic index types — allowing
numberorstringindex types would require runtime resolution, which is outside whatisLateBindableASThandles.
This is the minimum change needed to fix the reported bug without over-broadening the late-binding check [3].
TypeScript’s Computed Property Name Rules
TypeScript has specific rules for computed property names in object literals [3]:
| Expression Type | Allowed? | Reason |
|---|---|---|
string literal (e.g., ['foo']) | ✅ | Statically known |
number literal (e.g., [42]) | ✅ | Statically known |
Symbol (e.g., [Symbol.iterator]) | ✅ | Well-known symbol |
Enum member (e.g., [Keys.foo]) | ✅ | Resolves to literal type |
Enum member via element access (e.g., [Keys['foo']]) | ✅ (fixed) | Resolves to literal type, entity name |
| Variable / dynamic expression | ❌ | Cannot be resolved at compile time |
The fix specifically addresses the last row of “allowed” cases — enum members accessed via element access with a string literal [3].
[1]: See microsoft/TypeScript#25083 for the original issue. [2]: See microsoft/TypeScript#63526 for the fix PR and review. [3]: See TypeScript Handbook — Computed Property Names for the rules on computed properties.
Auto-generated from PR #25083. View all patches on GitHub.