From e0c46f83a48cd9caf98e8733b3be220a3985edca Mon Sep 17 00:00:00 2001 From: Scolliq <146259639+Scolliq@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:32:30 +0200 Subject: [PATCH] fix: allow bracket-notation enum access as computed property name in type positions Extends isLateBindableAST to recognise element access expressions of the form Enum['non-identifier-key'] (entity name indexed by a string/numeric literal) as late-bindable, in addition to the previously-supported Enum.Foo property-access style. Previously, enum members whose names are not valid identifiers (e.g. E['hello world'], E['3x14']) could not be used as computed property keys in type literals or interfaces, even though the expression evaluates to a well-known string literal type and the error message explicitly says that literal types are allowed. Fixes #25083 --- src/compiler/checker.ts | 9 +- .../enumBracketComputedPropertyName.js | 40 +++++++ .../enumBracketComputedPropertyName.symbols | 64 +++++++++++ .../enumBracketComputedPropertyName.types | 108 ++++++++++++++++++ .../isolatedDeclarationLazySymbols.errors.txt | 6 +- .../isolatedDeclarationLazySymbols.types | 4 +- .../enumBracketComputedPropertyName.ts | 25 ++++ 7 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 tests/baselines/reference/enumBracketComputedPropertyName.js create mode 100644 tests/baselines/reference/enumBracketComputedPropertyName.symbols create mode 100644 tests/baselines/reference/enumBracketComputedPropertyName.types create mode 100644 tests/cases/compiler/enumBracketComputedPropertyName.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0567712f11da3..fabc0787c5499 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -13790,7 +13790,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return false; } const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression; - return isEntityNameExpression(expr); + if (isEntityNameExpression(expr)) { + return true; + } + // Also allow element access on an entity name with a literal key, e.g. Enum['non-identifier-key']. + // This covers enum members whose names are not valid identifiers. + return isElementAccessExpression(expr) + && isEntityNameExpression(expr.expression) + && isStringOrNumericLiteralLike(skipParentheses(expr.argumentExpression)); } function isTypeUsableAsIndexSignature(type: Type): boolean { diff --git a/tests/baselines/reference/enumBracketComputedPropertyName.js b/tests/baselines/reference/enumBracketComputedPropertyName.js new file mode 100644 index 0000000000000..2cdd9d34ab81f --- /dev/null +++ b/tests/baselines/reference/enumBracketComputedPropertyName.js @@ -0,0 +1,40 @@ +//// [tests/cases/compiler/enumBracketComputedPropertyName.ts] //// + +//// [enumBracketComputedPropertyName.ts] +// Enum members with non-identifier names should be usable as computed property +// keys in type literals, interfaces, and class members (GH#25083). + +enum E { + "hello world" = "hw", + "3x14" = "pi", + normal = "n", +} + +// type literal +type T1 = { [E["hello world"]]: string }; +type T2 = { [E["3x14"]]: boolean }; +type T3 = { [E["normal"]]: number }; // bracket access to a normal-identifier member + +// interface +interface I1 { + [E["hello world"]]: string; +} + +// access back through the computed key +declare const t1: T1; +const v1: string = t1[E["hello world"]]; +const v2: string = t1["hw"]; // literal value + + +//// [enumBracketComputedPropertyName.js] +"use strict"; +// Enum members with non-identifier names should be usable as computed property +// keys in type literals, interfaces, and class members (GH#25083). +var E; +(function (E) { + E["hello world"] = "hw"; + E["3x14"] = "pi"; + E["normal"] = "n"; +})(E || (E = {})); +const v1 = t1[E["hello world"]]; +const v2 = t1["hw"]; // literal value diff --git a/tests/baselines/reference/enumBracketComputedPropertyName.symbols b/tests/baselines/reference/enumBracketComputedPropertyName.symbols new file mode 100644 index 0000000000000..d219864459b08 --- /dev/null +++ b/tests/baselines/reference/enumBracketComputedPropertyName.symbols @@ -0,0 +1,64 @@ +//// [tests/cases/compiler/enumBracketComputedPropertyName.ts] //// + +=== enumBracketComputedPropertyName.ts === +// Enum members with non-identifier names should be usable as computed property +// keys in type literals, interfaces, and class members (GH#25083). + +enum E { +>E : Symbol(E, Decl(enumBracketComputedPropertyName.ts, 0, 0)) + + "hello world" = "hw", +>"hello world" : Symbol(E["hello world"], Decl(enumBracketComputedPropertyName.ts, 3, 8)) + + "3x14" = "pi", +>"3x14" : Symbol(E["3x14"], Decl(enumBracketComputedPropertyName.ts, 4, 25)) + + normal = "n", +>normal : Symbol(E.normal, Decl(enumBracketComputedPropertyName.ts, 5, 18)) +} + +// type literal +type T1 = { [E["hello world"]]: string }; +>T1 : Symbol(T1, Decl(enumBracketComputedPropertyName.ts, 7, 1)) +>[E["hello world"]] : Symbol([E["hello world"]], Decl(enumBracketComputedPropertyName.ts, 10, 11)) +>E : Symbol(E, Decl(enumBracketComputedPropertyName.ts, 0, 0)) +>"hello world" : Symbol(E["hello world"], Decl(enumBracketComputedPropertyName.ts, 3, 8)) + +type T2 = { [E["3x14"]]: boolean }; +>T2 : Symbol(T2, Decl(enumBracketComputedPropertyName.ts, 10, 41)) +>[E["3x14"]] : Symbol([E["3x14"]], Decl(enumBracketComputedPropertyName.ts, 11, 11)) +>E : Symbol(E, Decl(enumBracketComputedPropertyName.ts, 0, 0)) +>"3x14" : Symbol(E["3x14"], Decl(enumBracketComputedPropertyName.ts, 4, 25)) + +type T3 = { [E["normal"]]: number }; // bracket access to a normal-identifier member +>T3 : Symbol(T3, Decl(enumBracketComputedPropertyName.ts, 11, 35)) +>[E["normal"]] : Symbol([E["normal"]], Decl(enumBracketComputedPropertyName.ts, 12, 11)) +>E : Symbol(E, Decl(enumBracketComputedPropertyName.ts, 0, 0)) +>"normal" : Symbol(E.normal, Decl(enumBracketComputedPropertyName.ts, 5, 18)) + +// interface +interface I1 { +>I1 : Symbol(I1, Decl(enumBracketComputedPropertyName.ts, 12, 36)) + + [E["hello world"]]: string; +>[E["hello world"]] : Symbol(I1[E["hello world"]], Decl(enumBracketComputedPropertyName.ts, 15, 14)) +>E : Symbol(E, Decl(enumBracketComputedPropertyName.ts, 0, 0)) +>"hello world" : Symbol(E["hello world"], Decl(enumBracketComputedPropertyName.ts, 3, 8)) +} + +// access back through the computed key +declare const t1: T1; +>t1 : Symbol(t1, Decl(enumBracketComputedPropertyName.ts, 20, 13)) +>T1 : Symbol(T1, Decl(enumBracketComputedPropertyName.ts, 7, 1)) + +const v1: string = t1[E["hello world"]]; +>v1 : Symbol(v1, Decl(enumBracketComputedPropertyName.ts, 21, 5)) +>t1 : Symbol(t1, Decl(enumBracketComputedPropertyName.ts, 20, 13)) +>E : Symbol(E, Decl(enumBracketComputedPropertyName.ts, 0, 0)) +>"hello world" : Symbol(E["hello world"], Decl(enumBracketComputedPropertyName.ts, 3, 8)) + +const v2: string = t1["hw"]; // literal value +>v2 : Symbol(v2, Decl(enumBracketComputedPropertyName.ts, 22, 5)) +>t1 : Symbol(t1, Decl(enumBracketComputedPropertyName.ts, 20, 13)) +>"hw" : Symbol([E["hello world"]], Decl(enumBracketComputedPropertyName.ts, 10, 11)) + diff --git a/tests/baselines/reference/enumBracketComputedPropertyName.types b/tests/baselines/reference/enumBracketComputedPropertyName.types new file mode 100644 index 0000000000000..47079913a1629 --- /dev/null +++ b/tests/baselines/reference/enumBracketComputedPropertyName.types @@ -0,0 +1,108 @@ +//// [tests/cases/compiler/enumBracketComputedPropertyName.ts] //// + +=== enumBracketComputedPropertyName.ts === +// Enum members with non-identifier names should be usable as computed property +// keys in type literals, interfaces, and class members (GH#25083). + +enum E { +>E : E +> : ^ + + "hello world" = "hw", +>"hello world" : (typeof E)["hello world"] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>"hw" : "hw" +> : ^^^^ + + "3x14" = "pi", +>"3x14" : (typeof E)["3x14"] +> : ^^^^^^^^^^^^^^^^^^ +>"pi" : "pi" +> : ^^^^ + + normal = "n", +>normal : E.normal +> : ^^^^^^^^ +>"n" : "n" +> : ^^^ +} + +// type literal +type T1 = { [E["hello world"]]: string }; +>T1 : T1 +> : ^^ +>[E["hello world"]] : string +> : ^^^^^^ +>E["hello world"] : (typeof E)["hello world"] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>E : typeof E +> : ^^^^^^^^ +>"hello world" : "hello world" +> : ^^^^^^^^^^^^^ + +type T2 = { [E["3x14"]]: boolean }; +>T2 : T2 +> : ^^ +>[E["3x14"]] : boolean +> : ^^^^^^^ +>E["3x14"] : (typeof E)["3x14"] +> : ^^^^^^^^^^^^^^^^^^ +>E : typeof E +> : ^^^^^^^^ +>"3x14" : "3x14" +> : ^^^^^^ + +type T3 = { [E["normal"]]: number }; // bracket access to a normal-identifier member +>T3 : T3 +> : ^^ +>[E["normal"]] : number +> : ^^^^^^ +>E["normal"] : E.normal +> : ^^^^^^^^ +>E : typeof E +> : ^^^^^^^^ +>"normal" : "normal" +> : ^^^^^^^^ + +// interface +interface I1 { + [E["hello world"]]: string; +>[E["hello world"]] : string +> : ^^^^^^ +>E["hello world"] : (typeof E)["hello world"] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>E : typeof E +> : ^^^^^^^^ +>"hello world" : "hello world" +> : ^^^^^^^^^^^^^ +} + +// access back through the computed key +declare const t1: T1; +>t1 : T1 +> : ^^ + +const v1: string = t1[E["hello world"]]; +>v1 : string +> : ^^^^^^ +>t1[E["hello world"]] : string +> : ^^^^^^ +>t1 : T1 +> : ^^ +>E["hello world"] : (typeof E)["hello world"] +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>E : typeof E +> : ^^^^^^^^ +>"hello world" : "hello world" +> : ^^^^^^^^^^^^^ + +const v2: string = t1["hw"]; // literal value +>v2 : string +> : ^^^^^^ +>t1["hw"] : string +> : ^^^^^^ +>t1 : T1 +> : ^^ +>"hw" : "hw" +> : ^^^^ + diff --git a/tests/baselines/reference/isolatedDeclarationLazySymbols.errors.txt b/tests/baselines/reference/isolatedDeclarationLazySymbols.errors.txt index 812ec2c1bea83..6931ab5f5df2b 100644 --- a/tests/baselines/reference/isolatedDeclarationLazySymbols.errors.txt +++ b/tests/baselines/reference/isolatedDeclarationLazySymbols.errors.txt @@ -1,6 +1,6 @@ isolatedDeclarationLazySymbols.ts(1,17): error TS9007: Function must have an explicit return type annotation with --isolatedDeclarations. +isolatedDeclarationLazySymbols.ts(12,1): error TS9023: Assigning properties to functions without declaring them is not supported with --isolatedDeclarations. Add an explicit declaration for the properties assigned to this function. isolatedDeclarationLazySymbols.ts(13,1): error TS9023: Assigning properties to functions without declaring them is not supported with --isolatedDeclarations. Add an explicit declaration for the properties assigned to this function. -isolatedDeclarationLazySymbols.ts(16,5): error TS1166: A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type. isolatedDeclarationLazySymbols.ts(16,5): error TS9038: Computed property names on class or object literals cannot be inferred with --isolatedDeclarations. isolatedDeclarationLazySymbols.ts(21,5): error TS9038: Computed property names on class or object literals cannot be inferred with --isolatedDeclarations. isolatedDeclarationLazySymbols.ts(22,5): error TS9038: Computed property names on class or object literals cannot be inferred with --isolatedDeclarations. @@ -22,6 +22,8 @@ isolatedDeclarationLazySymbols.ts(22,5): error TS9038: Computed property names o } as const foo[o["prop.inner"]] ="A"; + ~~~~~~~~~~~~~~~~~~~~ +!!! error TS9023: Assigning properties to functions without declaring them is not supported with --isolatedDeclarations. Add an explicit declaration for the properties assigned to this function. foo[o.prop.inner] = "B"; ~~~~~~~~~~~~~~~~~ !!! error TS9023: Assigning properties to functions without declaring them is not supported with --isolatedDeclarations. Add an explicit declaration for the properties assigned to this function. @@ -29,8 +31,6 @@ isolatedDeclarationLazySymbols.ts(22,5): error TS9038: Computed property names o export class Foo { [o["prop.inner"]] ="A" ~~~~~~~~~~~~~~~~~ -!!! error TS1166: A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type. - ~~~~~~~~~~~~~~~~~ !!! error TS9038: Computed property names on class or object literals cannot be inferred with --isolatedDeclarations. [o.prop.inner] = "B" } diff --git a/tests/baselines/reference/isolatedDeclarationLazySymbols.types b/tests/baselines/reference/isolatedDeclarationLazySymbols.types index c7e91f9680a8c..5a1bfe22638bd 100644 --- a/tests/baselines/reference/isolatedDeclarationLazySymbols.types +++ b/tests/baselines/reference/isolatedDeclarationLazySymbols.types @@ -40,8 +40,8 @@ const o = { foo[o["prop.inner"]] ="A"; >foo[o["prop.inner"]] ="A" : "A" > : ^^^ ->foo[o["prop.inner"]] : any -> : ^^^ +>foo[o["prop.inner"]] : string +> : ^^^^^^ >foo : typeof foo > : ^^^^^^^^^^ >o["prop.inner"] : "a" diff --git a/tests/cases/compiler/enumBracketComputedPropertyName.ts b/tests/cases/compiler/enumBracketComputedPropertyName.ts new file mode 100644 index 0000000000000..3be15f1e8ad02 --- /dev/null +++ b/tests/cases/compiler/enumBracketComputedPropertyName.ts @@ -0,0 +1,25 @@ +// @strict: true + +// Enum members with non-identifier names should be usable as computed property +// keys in type literals, interfaces, and class members (GH#25083). + +enum E { + "hello world" = "hw", + "3x14" = "pi", + normal = "n", +} + +// type literal +type T1 = { [E["hello world"]]: string }; +type T2 = { [E["3x14"]]: boolean }; +type T3 = { [E["normal"]]: number }; // bracket access to a normal-identifier member + +// interface +interface I1 { + [E["hello world"]]: string; +} + +// access back through the computed key +declare const t1: T1; +const v1: string = t1[E["hello world"]]; +const v2: string = t1["hw"]; // literal value