Fix: Enum keys not accepted as computed properties with non-identifier names
Fixed microsoft/TypeScript#25083 — 3 line bug-fix.

The Bug
Repo: microsoft/TypeScript Issue: #25083 Status: closed-not-merged PR: https://github.com/microsoft/TypeScript/pull/63526
Description: Enum keys not accepted as computed properties with non-identifier names
Fix scope: 3 lines added to isLateBindableAST() in src/compiler/checker.ts
Root Cause
The bug lives in src/compiler/checker.ts inside the isLateBindableAST() function. This function determines whether a computed property name expression can be resolved during type checking — i.e., whether it is “late bindable.” The original code checked if the expression was an entity name, which only covers simple identifiers (Foo) and qualified names (A.B.C). It did not handle ElementAccessExpression nodes — bracket-notation access like Type['3x14'] — even though these are trivially resolvable at type-checking time.
The failing pattern:
enum Keys { 'my-key' = 'my-key', 'foo-bar' = 'foo-bar' }
type T = { [Keys['my-key']]: string } // Error 1170
The expression Keys['my-key'] is an ElementAccessExpression with:
- expression: Identifier
Keys(the enum) - argumentExpression: StringLiteral
'my-key'(the key)
Because isEntityNameExpression() only handles Identifier and QualifiedName AST nodes, the ElementAccessExpression is rejected outright. The compiler emits error 1170: “A computed property name in a type literal must refer to an expression whose type is a literal type or a ‘unique symbol’ type.” This is a false positive — Keys['my-key'] trivially resolves to the string literal 'my-key' at compile time, which is a perfectly valid computed property key.
Impact: The bug affects any TypeScript code that uses bracket-access on enums inside computed property names with a string literal argument. While dot-access (Keys.Foo) works for identifier-compliant names, bracket-access fails unconditionally — even for valid identifier keys like Keys['Foo'], because the problem is the AST node shape, not the key name validity. Developers are forced to rename enum members or restructure their type definitions to work around the false positive.
Code Analysis
The isLateBindableAST() function sits at a specific point in TypeScript’s computed property name validation pipeline inside src/compiler/checker.ts:
checkComputedPropertyName()
→ isLateBindableAST(node) // <-- the bug
→ checkExpressionCached(expr) // type-check the expression
→ isTypeUsableAsIndexSignature() // verify result is a valid key type
The original implementation:
function isLateBindableAST(node: DeclarationName) {
if (!isComputedPropertyName(node) && !isElementAccessExpression(node)) {
return false;
}
const expr = isComputedPropertyName(node)
? node.expression
: node.argumentExpression;
return isEntityNameExpression(expr);
}
isEntityNameExpression() returns true only for Identifier and QualifiedName syntax kinds. This is correct for most cases — a computed property like [someVariable] is genuinely dynamic and cannot be late-bound. But Keys['3x14'] is semantically identical to Keys.Foo — the enum reference and the string key are both fully resolved at compile time. The only difference is the AST shape (ElementAccessExpression vs. PropertyAccessExpression), yet the type checker rejects one and accepts the other.
The root design issue is that isLateBindableAST tests the syntactic form of the expression rather than its semantic resolvability. A string literal inside a bracket access is a compile-time constant. The enum Keys is a known entity whose type is computed synchronously. There is no runtime dependency or dynamic dispatch — the entire expression resolves in a single synchronous type-table lookup. Rejecting it is a blind spot in the coverage of valid property key expressions.
The Fix
The fix adds a single disjunctive condition to the isLateBindableAST() return statement, expanding acceptance without refactoring any existing logic:
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 added condition matches three criteria simultaneously:
isElementAccessExpression(expr)— the computed property key is a bracket-access expression likeKeys['key']isStringLiteral(expr.argumentExpression)— the bracket argument is a string literal, not a variable or complex expressionisEntityNameExpression(expr.expression)— the object being accessed (e.g.,Keys) is a resolvable entity name
This is a strictly restrictive addition: it only accepts the specific nested pattern of EntityName[StringLiteral]. Dynamic keys (Keys[someVariable]), computed keys on non-entity objects (foo['bar'] where foo is not an entity name), and non-literal bracket arguments remain correctly rejected. The fix expands the set of accepted programs without ever rejecting code the original function would have accepted — making the change trivially safe to review and merge.
Pattern & Takeaways
Pattern: AST shape mismatch — the code assumed that isEntityNameExpression covered all statically resolvable property name expressions, but it only covers syntactic forms (Identifier, QualifiedName), not semantically equivalent forms (ElementAccessExpression on an entity with a literal argument). The code path handling bracket-access existed in the broader computed property name pipeline (checkComputedPropertyName) but isLateBindableAST was not updated when bracket-access on enums was added as a supported idiom.
Key insight: The most predictable bugs live at the boundaries between syntactic validity and semantic equivalence. When a language adds syntactic sugar or alternative access patterns (bracket-notation vs. dot-notation), every downstream guard function must be audited independently. Here, isEntityNameExpression was correct for value-position code but too narrow for computed-property-name position. The fix’s three-guard pattern (isElementAccessExpression + isStringLiteral + isEntityNameExpression) is a reusable template for accepting bracket-access on known entities while rejecting truly dynamic expressions.
Code review heuristic: When reviewing changes that add new syntax forms to existing checks, ask: (1) Is this guard function’s contract broader or narrower than its name implies? (2) Does the function test the syntactic shape or the semantic resolvability of the node? (3) Are there alternative access patterns (bracket-access, computed access, indexed access) that resolve to the same runtime value but take different AST forms?
Transfer Potential
Varies — edge case fixes are repo-specific in detail but universal in pattern. The minimal-change principle and boundary-condition thinking transfer to any codebase. Reading this post helps recognize similar patterns in your own projects.
Auto-generated from PR #25083. View all patches on GitHub.
📖 Related Reads
- ToolBrain — tool reviews, LLM comparisons, and AI workflow guides
Cross-links automatically generated from CodeIntel Log.