Skip to content

fix(checker): Allow element access expressions in computed property names if argument is literal #62968

Open
kairosci wants to merge 10 commits intomicrosoft:mainfrom
kairosci:fix/enum-computed-keys
Open

fix(checker): Allow element access expressions in computed property names if argument is literal #62968
kairosci wants to merge 10 commits intomicrosoft:mainfrom
kairosci:fix/enum-computed-keys

Conversation

@kairosci
Copy link

@kairosci kairosci commented Jan 9, 2026

Fixes #25083.

This PR relaxes the check for computed property names in type literals (specifically in isLateBindableAST) to allow ElementAccessExpression when the argument is a static literal.

Changes

Checker (src/compiler/checker.ts)

  • Updated isLateBindableAST to use a new helper isLateBindableExpression.
  • isLateBindableExpression recursively checks the expression and allows:
    • String literals (e.g., Enum['key'])
    • Numeric literals
    • Signed numeric literals (e.g., Enum[-1])
    • Parenthesized expressions (e.g., Enum[('key')])

Utilities (src/compiler/utilities.ts)

  • Updated isComputedNonLiteralName to also check for isSignedNumericLiteral
  • Updated tryGetTextOfPropertyName to handle signed numeric literals in ComputedPropertyName, aligning with getPropertyNameForPropertyNameNode

Example

enum Type {
  '3x14' = '3x14'
}
enum Numeric {
    Negative = -1,
}

type TypeMap = {
  [Type['3x14']]: any;       // Now valid
  [Numeric['Negative']]: any; // Now valid
  [Numeric[('Negative')]]: any; // Now valid
}

Verified with new test case tests/cases/compiler/enumKeysInTypeLiteral.ts covering these scenarios.

@github-project-automation github-project-automation bot moved this to Not started in PR Backlog Jan 9, 2026
@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Jan 9, 2026
@kairosci kairosci marked this pull request as draft January 9, 2026 22:07
@kairosci kairosci force-pushed the fix/enum-computed-keys branch from c7b8d77 to 3784ace Compare January 9, 2026 22:09
@kairosci kairosci marked this pull request as ready for review January 9, 2026 22:12
@kairosci kairosci changed the title Fix(checker): Allow element access expressions in computed property names if argument is literal (Fixes #25083) fix(checker): Allow element access expressions in computed property names if argument is literal Jan 9, 2026
@kairosci kairosci force-pushed the fix/enum-computed-keys branch from 3784ace to e813bb0 Compare January 9, 2026 22:25
@kairosci kairosci force-pushed the fix/enum-computed-keys branch from 99b7bff to 91424d8 Compare January 15, 2026 16:45
Copilot AI review requested due to automatic review settings February 4, 2026 08:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes issue #25083 by allowing element access expressions (e.g., Enum['key']) as computed property names in type literals when the argument is a literal. Previously, only dot notation (e.g., Enum.key) was accepted for enum members in computed property names.

Changes:

  • Modified isLateBindableAST to use a new helper isLateBindableExpression that recursively validates element access chains with literal arguments
  • Updated utility functions to handle signed numeric literals in computed property names
  • Added test case covering enum keys accessed via bracket notation in type literals

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/cases/compiler/enumKeysInTypeLiteral.ts New test case demonstrating enum element access with bracket notation in type literals
tests/baselines/reference/enumKeysInTypeLiteral.* Baseline files for the new test showing expected types, symbols, and JS output
tests/baselines/reference/isolatedDeclarationLazySymbols.* Updated baselines reflecting improved type inference for element access expressions
src/compiler/checker.ts Added isLateBindableExpression helper and modified isLateBindableAST to handle element access chains
src/compiler/utilities.ts Updated isComputedNonLiteralName and tryGetTextOfPropertyName to handle signed numeric literals; formatting improvements

Comment on lines 13752 to 13753
else if (isElementAccessExpression(node)) {
return isLateBindableExpression(node.argumentExpression);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for handling ElementAccessExpression is incorrect. When node is an ElementAccessExpression (e.g., Type['key']), this code passes only node.argumentExpression (which would be the literal 'key') to isLateBindableExpression.

However, isLateBindableExpression expects to validate the full expression chain. When passed a literal like 'key', it skips the while loop (since a literal is not an ElementAccessExpression) and calls isEntityNameExpression('key'), which returns false because a string literal is not an entity name expression.

The correct approach is to pass the entire node to isLateBindableExpression, not just node.argumentExpression. This would allow the function to properly validate the element access chain and then check that the base is an entity name expression.

Copilot uses AI. Check for mistakes.
Fixes the bug identified by Copilot AI review where isLateBindableAST
was incorrectly passing only node.argumentExpression instead of the
entire node to isLateBindableExpression. This ensures proper validation
of the full element access chain.

The fix makes the behavior more strict: only syntactic literals are
accepted in element access expressions (e.g., Type['3x14']), not
variables with literal types (e.g., Type[variable]).
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 40 changed files in this pull request and generated 2 comments.

Comment on lines 2786 to 11708
@@ -3179,7 +3187,7 @@ export function getThisContainer(node: Node, includeArrowFunctions: boolean, inc
if (!includeArrowFunctions) {
continue;
}
// falls through
// falls through

case SyntaxKind.FunctionDeclaration:
case SyntaxKind.FunctionExpression:
@@ -3304,7 +3312,7 @@ export function getSuperContainer(node: Node, stopOnFunctions: boolean) {
if (!stopOnFunctions) {
continue;
}
// falls through
// falls through

case SyntaxKind.PropertyDeclaration:
case SyntaxKind.PropertySignature:
@@ -3640,7 +3648,7 @@ export function isExpressionNode(node: Node): boolean {
if (node.parent.kind === SyntaxKind.TypeQuery || isJSDocLinkLike(node.parent) || isJSDocNameReference(node.parent) || isJSDocMemberName(node.parent) || isJSXTagName(node)) {
return true;
}
// falls through
// falls through

case SyntaxKind.NumericLiteral:
case SyntaxKind.BigIntLiteral:
@@ -5036,7 +5044,7 @@ export function getDeclarationFromName(name: Node): Declaration | undefined {
case SyntaxKind.NoSubstitutionTemplateLiteral:
case SyntaxKind.NumericLiteral:
if (isComputedPropertyName(parent)) return parent.parent;
// falls through
// falls through
case SyntaxKind.Identifier:
if (isDeclaration(parent)) {
return parent.name === name ? parent : undefined;
@@ -5281,7 +5289,7 @@ export function getFunctionFlags(node: SignatureDeclaration | undefined): Functi
if (node.asteriskToken) {
flags |= FunctionFlags.Generator;
}
// falls through
// falls through

case SyntaxKind.ArrowFunction:
if (hasSyntacticModifier(node, ModifierFlags.Async)) {
@@ -8393,7 +8401,7 @@ export function getLeftmostExpression(node: Expression, stopAtCallExpressions: b
if (stopAtCallExpressions) {
return node;
}
// falls through
// falls through
case SyntaxKind.AsExpression:
case SyntaxKind.ElementAccessExpression:
case SyntaxKind.PropertyAccessExpression:
@@ -11551,7 +11559,7 @@ export function createNameResolver({
switch (location.kind) {
case SyntaxKind.SourceFile:
if (!isExternalOrCommonJsModule(location as SourceFile)) break;
// falls through
// falls through
case SyntaxKind.ModuleDeclaration:
const moduleExports = getSymbolOfDeclaration(location as SourceFile | ModuleDeclaration)?.exports || emptySymbols;
if (location.kind === SyntaxKind.SourceFile || (isModuleDeclaration(location) && location.flags & NodeFlags.Ambient && !isGlobalScopeAugmentation(location))) {
@@ -11697,7 +11705,7 @@ export function createNameResolver({
if (getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2015) {
break;
}
// falls through
// falls through
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fall-through comment indentation changes in this file appear to be unrelated to the main fix for enum keys in type literals. While the new formatting is correct, these changes should ideally be in a separate commit or PR focused on code style/formatting cleanup.

If these changes are intentional cleanup, they should be mentioned in the PR description. If they were accidental changes from an auto-formatter, consider reverting them to keep this PR focused on the bug fix.

Copilot uses AI. Check for mistakes.
function isLateBindableExpression(expr: Expression): boolean {
while (isElementAccessExpression(expr)) {
const argument = skipParentheses(expr.argumentExpression);
if (!isStringOrNumericLiteralLike(argument) && !isSignedNumericLiteral(argument)) return false;
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check in the while loop is too restrictive. It should also allow EntityNameExpression (identifier or property access) as arguments, not just literals.

The current code only accepts string/numeric literals or signed numeric literals, which breaks late-bound property assignments like foo[_private] = "ok" where _private is an identifier variable.

The condition should be:

if (!isStringOrNumericLiteralLike(argument) && 
    !isSignedNumericLiteral(argument) && 
    !isEntityNameExpression(argument)) {
    return false;
}

This allows both the new use case (Enum['key'] with literal arguments) and the existing use case (foo[symbol] with identifier arguments).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Backlog Bug PRs that fix a backlog bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

Enum keys not accepted as computed properties if their name is not a valid identifier

2 participants