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]

  1. String literals are statically known — the compiler can resolve 'my-key' at compile time, so late binding is safe.
  2. Entity name on the leftKeys must be a known entity (enum, namespace, class). This prevents arbitrary computed access like someObj[variable].
  3. Prevents dynamic index types — allowing number or string index types would require runtime resolution, which is outside what isLateBindableAST handles.

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 TypeAllowed?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 expressionCannot 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.