Skip to content

Commit ec84ad1

Browse files
committed
Allow enum keys accessed with bracket notation as computed properties
This fixes #25083 where enum keys accessed with bracket notation (e.g., `Type['3x14']`) were not recognized as valid computed property names in type literals, even when they resolved to literal types. The fix extends `isLateBindableAST` to recognize `ElementAccessExpression` with string or numeric literal keys as valid late-bindable expressions, similar to how `PropertyAccessExpression` is already handled. This also updates `checkGrammarForInvalidDynamicName` to allow such expressions as computed property names.
1 parent 16b933f commit ec84ad1

7 files changed

+319
-8
lines changed

src/compiler/checker.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13721,7 +13721,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1372113721
return false;
1372213722
}
1372313723
const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression;
13724-
return isEntityNameExpression(expr);
13724+
return isEntityNameOrElementAccessExpression(expr);
13725+
}
13726+
13727+
/**
13728+
* Returns true if the expression is a valid late-bindable expression.
13729+
* A late-bindable expression is an entity name expression (Identifier or PropertyAccessExpression)
13730+
* or an ElementAccessExpression with a string or numeric literal key where the base expression
13731+
* is itself a valid late-bindable expression.
13732+
*/
13733+
function isEntityNameOrElementAccessExpression(node: Node): boolean {
13734+
if (isEntityNameExpression(node)) {
13735+
return true;
13736+
}
13737+
if (isElementAccessExpression(node) && isStringOrNumericLiteralLike(node.argumentExpression)) {
13738+
return isEntityNameOrElementAccessExpression(node.expression);
13739+
}
13740+
return false;
1372513741
}
1372613742

1372713743
function isTypeUsableAsIndexSignature(type: Type): boolean {
@@ -52910,8 +52926,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
5291052926
}
5291152927

5291252928
function checkGrammarForInvalidDynamicName(node: DeclarationName, message: DiagnosticMessage) {
52913-
// Even non-bindable names are allowed as late-bound implied index signatures so long as the name is a simple `a.b.c` type name expression
52914-
if (isNonBindableDynamicName(node) && !isEntityNameExpression(isElementAccessExpression(node) ? skipParentheses(node.argumentExpression) : (node as ComputedPropertyName).expression)) {
52929+
// Even non-bindable names are allowed as late-bound implied index signatures so long as the name is a simple `a.b.c` or `a['b']` type name expression
52930+
if (isNonBindableDynamicName(node) && !isEntityNameOrElementAccessExpression(isElementAccessExpression(node) ? skipParentheses(node.argumentExpression) : (node as ComputedPropertyName).expression)) {
5291552931
return grammarErrorOnNode(node, message);
5291652932
}
5291752933
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//// [tests/cases/compiler/enumKeysAsComputedPropertiesWithBracketNotation.ts] ////
2+
3+
//// [enumKeysAsComputedPropertiesWithBracketNotation.ts]
4+
// Test that enum keys accessed with bracket notation can be used as computed properties
5+
// Regression test for https://github.com/microsoft/TypeScript/issues/25083
6+
7+
enum Type {
8+
Foo = 'foo',
9+
'3x14' = '3x14'
10+
}
11+
12+
// All of these should work
13+
type TypeMap = {
14+
[Type.Foo]: string; // Property access
15+
[Type['3x14']]: number; // Element access with non-identifier key
16+
}
17+
18+
// Bracket notation with identifier key should also work (equivalent to property access)
19+
type TypeMap2 = {
20+
[Type['Foo']]: boolean;
21+
}
22+
23+
// Nested element access should work
24+
const nested = {
25+
inner: {
26+
key: 'hello' as const
27+
}
28+
};
29+
30+
type TypeMap3 = {
31+
[nested.inner.key]: string;
32+
}
33+
34+
// Element access on deeply nested path
35+
type TypeMap4 = {
36+
[nested['inner']['key']]: string;
37+
}
38+
39+
40+
//// [enumKeysAsComputedPropertiesWithBracketNotation.js]
41+
"use strict";
42+
// Test that enum keys accessed with bracket notation can be used as computed properties
43+
// Regression test for https://github.com/microsoft/TypeScript/issues/25083
44+
var Type;
45+
(function (Type) {
46+
Type["Foo"] = "foo";
47+
Type["3x14"] = "3x14";
48+
})(Type || (Type = {}));
49+
// Nested element access should work
50+
var nested = {
51+
inner: {
52+
key: 'hello'
53+
}
54+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//// [tests/cases/compiler/enumKeysAsComputedPropertiesWithBracketNotation.ts] ////
2+
3+
=== enumKeysAsComputedPropertiesWithBracketNotation.ts ===
4+
// Test that enum keys accessed with bracket notation can be used as computed properties
5+
// Regression test for https://github.com/microsoft/TypeScript/issues/25083
6+
7+
enum Type {
8+
>Type : Symbol(Type, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 0, 0))
9+
10+
Foo = 'foo',
11+
>Foo : Symbol(Type.Foo, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 3, 11))
12+
13+
'3x14' = '3x14'
14+
>'3x14' : Symbol(Type['3x14'], Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 4, 16))
15+
}
16+
17+
// All of these should work
18+
type TypeMap = {
19+
>TypeMap : Symbol(TypeMap, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 6, 1))
20+
21+
[Type.Foo]: string; // Property access
22+
>[Type.Foo] : Symbol([Type.Foo], Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 9, 16))
23+
>Type.Foo : Symbol(Type.Foo, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 3, 11))
24+
>Type : Symbol(Type, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 0, 0))
25+
>Foo : Symbol(Type.Foo, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 3, 11))
26+
27+
[Type['3x14']]: number; // Element access with non-identifier key
28+
>[Type['3x14']] : Symbol([Type['3x14']], Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 10, 23))
29+
>Type : Symbol(Type, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 0, 0))
30+
>'3x14' : Symbol(Type['3x14'], Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 4, 16))
31+
}
32+
33+
// Bracket notation with identifier key should also work (equivalent to property access)
34+
type TypeMap2 = {
35+
>TypeMap2 : Symbol(TypeMap2, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 12, 1))
36+
37+
[Type['Foo']]: boolean;
38+
>[Type['Foo']] : Symbol([Type['Foo']], Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 15, 17))
39+
>Type : Symbol(Type, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 0, 0))
40+
>'Foo' : Symbol(Type.Foo, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 3, 11))
41+
}
42+
43+
// Nested element access should work
44+
const nested = {
45+
>nested : Symbol(nested, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 20, 5))
46+
47+
inner: {
48+
>inner : Symbol(inner, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 20, 16))
49+
50+
key: 'hello' as const
51+
>key : Symbol(key, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 21, 12))
52+
>const : Symbol(const)
53+
}
54+
};
55+
56+
type TypeMap3 = {
57+
>TypeMap3 : Symbol(TypeMap3, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 24, 2))
58+
59+
[nested.inner.key]: string;
60+
>[nested.inner.key] : Symbol([nested.inner.key], Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 26, 17))
61+
>nested.inner.key : Symbol(key, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 21, 12))
62+
>nested.inner : Symbol(inner, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 20, 16))
63+
>nested : Symbol(nested, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 20, 5))
64+
>inner : Symbol(inner, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 20, 16))
65+
>key : Symbol(key, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 21, 12))
66+
}
67+
68+
// Element access on deeply nested path
69+
type TypeMap4 = {
70+
>TypeMap4 : Symbol(TypeMap4, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 28, 1))
71+
72+
[nested['inner']['key']]: string;
73+
>[nested['inner']['key']] : Symbol([nested['inner']['key']], Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 31, 17))
74+
>nested : Symbol(nested, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 20, 5))
75+
>'inner' : Symbol(inner, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 20, 16))
76+
>'key' : Symbol(key, Decl(enumKeysAsComputedPropertiesWithBracketNotation.ts, 21, 12))
77+
}
78+
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//// [tests/cases/compiler/enumKeysAsComputedPropertiesWithBracketNotation.ts] ////
2+
3+
=== enumKeysAsComputedPropertiesWithBracketNotation.ts ===
4+
// Test that enum keys accessed with bracket notation can be used as computed properties
5+
// Regression test for https://github.com/microsoft/TypeScript/issues/25083
6+
7+
enum Type {
8+
>Type : Type
9+
> : ^^^^
10+
11+
Foo = 'foo',
12+
>Foo : Type.Foo
13+
> : ^^^^^^^^
14+
>'foo' : "foo"
15+
> : ^^^^^
16+
17+
'3x14' = '3x14'
18+
>'3x14' : (typeof Type)["3x14"]
19+
> : ^^^^^^^^^^^^^^^^^^^^^
20+
>'3x14' : "3x14"
21+
> : ^^^^^^
22+
}
23+
24+
// All of these should work
25+
type TypeMap = {
26+
>TypeMap : TypeMap
27+
> : ^^^^^^^
28+
29+
[Type.Foo]: string; // Property access
30+
>[Type.Foo] : string
31+
> : ^^^^^^
32+
>Type.Foo : Type.Foo
33+
> : ^^^^^^^^
34+
>Type : typeof Type
35+
> : ^^^^^^^^^^^
36+
>Foo : Type.Foo
37+
> : ^^^^^^^^
38+
39+
[Type['3x14']]: number; // Element access with non-identifier key
40+
>[Type['3x14']] : number
41+
> : ^^^^^^
42+
>Type['3x14'] : (typeof Type)["3x14"]
43+
> : ^^^^^^^^^^^^^^^^^^^^^
44+
>Type : typeof Type
45+
> : ^^^^^^^^^^^
46+
>'3x14' : "3x14"
47+
> : ^^^^^^
48+
}
49+
50+
// Bracket notation with identifier key should also work (equivalent to property access)
51+
type TypeMap2 = {
52+
>TypeMap2 : TypeMap2
53+
> : ^^^^^^^^
54+
55+
[Type['Foo']]: boolean;
56+
>[Type['Foo']] : boolean
57+
> : ^^^^^^^
58+
>Type['Foo'] : Type.Foo
59+
> : ^^^^^^^^
60+
>Type : typeof Type
61+
> : ^^^^^^^^^^^
62+
>'Foo' : "Foo"
63+
> : ^^^^^
64+
}
65+
66+
// Nested element access should work
67+
const nested = {
68+
>nested : { inner: { key: "hello"; }; }
69+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
70+
>{ inner: { key: 'hello' as const }} : { inner: { key: "hello"; }; }
71+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
72+
73+
inner: {
74+
>inner : { key: "hello"; }
75+
> : ^^^^^^^^^^^^^^^^^
76+
>{ key: 'hello' as const } : { key: "hello"; }
77+
> : ^^^^^^^^^^^^^^^^^
78+
79+
key: 'hello' as const
80+
>key : "hello"
81+
> : ^^^^^^^
82+
>'hello' as const : "hello"
83+
> : ^^^^^^^
84+
>'hello' : "hello"
85+
> : ^^^^^^^
86+
}
87+
};
88+
89+
type TypeMap3 = {
90+
>TypeMap3 : TypeMap3
91+
> : ^^^^^^^^
92+
93+
[nested.inner.key]: string;
94+
>[nested.inner.key] : string
95+
> : ^^^^^^
96+
>nested.inner.key : "hello"
97+
> : ^^^^^^^
98+
>nested.inner : { key: "hello"; }
99+
> : ^^^^^^^^^^^^^^^^^
100+
>nested : { inner: { key: "hello"; }; }
101+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
102+
>inner : { key: "hello"; }
103+
> : ^^^^^^^^^^^^^^^^^
104+
>key : "hello"
105+
> : ^^^^^^^
106+
}
107+
108+
// Element access on deeply nested path
109+
type TypeMap4 = {
110+
>TypeMap4 : TypeMap4
111+
> : ^^^^^^^^
112+
113+
[nested['inner']['key']]: string;
114+
>[nested['inner']['key']] : string
115+
> : ^^^^^^
116+
>nested['inner']['key'] : "hello"
117+
> : ^^^^^^^
118+
>nested['inner'] : { key: "hello"; }
119+
> : ^^^^^^^^^^^^^^^^^
120+
>nested : { inner: { key: "hello"; }; }
121+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
122+
>'inner' : "inner"
123+
> : ^^^^^^^
124+
>'key' : "key"
125+
> : ^^^^^
126+
}
127+

tests/baselines/reference/isolatedDeclarationLazySymbols.errors.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
isolatedDeclarationLazySymbols.ts(1,17): error TS9007: Function must have an explicit return type annotation with --isolatedDeclarations.
2+
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.
23
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.
3-
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.
44
isolatedDeclarationLazySymbols.ts(16,5): error TS9038: Computed property names on class or object literals cannot be inferred with --isolatedDeclarations.
55
isolatedDeclarationLazySymbols.ts(21,5): error TS9038: Computed property names on class or object literals cannot be inferred with --isolatedDeclarations.
66
isolatedDeclarationLazySymbols.ts(22,5): error TS9038: Computed property names on class or object literals cannot be inferred with --isolatedDeclarations.
@@ -22,15 +22,15 @@ isolatedDeclarationLazySymbols.ts(22,5): error TS9038: Computed property names o
2222
} as const
2323

2424
foo[o["prop.inner"]] ="A";
25+
~~~~~~~~~~~~~~~~~~~~
26+
!!! 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.
2527
foo[o.prop.inner] = "B";
2628
~~~~~~~~~~~~~~~~~
2729
!!! 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.
2830

2931
export class Foo {
3032
[o["prop.inner"]] ="A"
3133
~~~~~~~~~~~~~~~~~
32-
!!! error TS1166: A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type.
33-
~~~~~~~~~~~~~~~~~
3434
!!! error TS9038: Computed property names on class or object literals cannot be inferred with --isolatedDeclarations.
3535
[o.prop.inner] = "B"
3636
}

tests/baselines/reference/isolatedDeclarationLazySymbols.types

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ const o = {
4040
foo[o["prop.inner"]] ="A";
4141
>foo[o["prop.inner"]] ="A" : "A"
4242
> : ^^^
43-
>foo[o["prop.inner"]] : any
44-
> : ^^^
43+
>foo[o["prop.inner"]] : string
44+
> : ^^^^^^
4545
>foo : typeof foo
4646
> : ^^^^^^^^^^
4747
>o["prop.inner"] : "a"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// @strict: true
2+
3+
// Test that enum keys accessed with bracket notation can be used as computed properties
4+
// Regression test for https://github.com/microsoft/TypeScript/issues/25083
5+
6+
enum Type {
7+
Foo = 'foo',
8+
'3x14' = '3x14'
9+
}
10+
11+
// All of these should work
12+
type TypeMap = {
13+
[Type.Foo]: string; // Property access
14+
[Type['3x14']]: number; // Element access with non-identifier key
15+
}
16+
17+
// Bracket notation with identifier key should also work (equivalent to property access)
18+
type TypeMap2 = {
19+
[Type['Foo']]: boolean;
20+
}
21+
22+
// Nested element access should work
23+
const nested = {
24+
inner: {
25+
key: 'hello' as const
26+
}
27+
};
28+
29+
type TypeMap3 = {
30+
[nested.inner.key]: string;
31+
}
32+
33+
// Element access on deeply nested path
34+
type TypeMap4 = {
35+
[nested['inner']['key']]: string;
36+
}

0 commit comments

Comments
 (0)