Skip to content

Commit 1d0e474

Browse files
committed
fix(declarations): preserve element access computed properties in .d.ts
- Fix declaration emit elide condition to recognize ElementAccessExpression as a valid late-bindable expression (declarations.ts:1026) - Update isolatedDeclarations check to accept late-bindable access expressions (declarations.ts:1019) - Add LateBindableAccessExpression type alias for clearer semantics - Extract isLateBindableAccessExpression to utilities.ts and remove duplicate implementation from checker.ts - Update getFirstIdentifier to support ElementAccessExpression chains - Update isEntityNameVisible and related APIs to accept ElementAccessExpression - Add test case with @declaration: true to verify computed properties are preserved in generated .d.ts files
1 parent be051c4 commit 1d0e474

File tree

5 files changed

+92
-48
lines changed

5 files changed

+92
-48
lines changed

src/compiler/checker.ts

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ import {
655655
isJsxSpreadAttribute,
656656
isJSXTagName,
657657
isKnownSymbol,
658+
isLateBindableAccessExpression,
658659
isLateVisibilityPaintedStatement,
659660
isLeftHandSideExpression,
660661
isLineBreak,
@@ -6042,8 +6043,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
60426043
}
60436044
}
60446045

6045-
function getMeaningOfEntityNameReference(entityName: EntityNameOrEntityNameExpression): SymbolFlags {
6046-
// get symbol of the first identifier of the entityName
6046+
function getMeaningOfEntityNameReference(entityName: EntityNameOrEntityNameExpression | ElementAccessExpression): SymbolFlags {
6047+
// get symbol of the first identifier of the entityName or element access chain
60476048
let meaning: SymbolFlags;
60486049
if (
60496050
entityName.parent.kind === SyntaxKind.TypeQuery ||
@@ -6056,6 +6057,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
60566057
}
60576058
else if (
60586059
entityName.kind === SyntaxKind.QualifiedName || entityName.kind === SyntaxKind.PropertyAccessExpression ||
6060+
entityName.kind === SyntaxKind.ElementAccessExpression ||
60596061
entityName.parent.kind === SyntaxKind.ImportEqualsDeclaration ||
60606062
(entityName.parent.kind === SyntaxKind.QualifiedName && (entityName.parent as QualifiedName).left === entityName) ||
60616063
(entityName.parent.kind === SyntaxKind.PropertyAccessExpression && (entityName.parent as PropertyAccessExpression).expression === entityName) ||
@@ -6072,7 +6074,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
60726074
return meaning;
60736075
}
60746076

6075-
function isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node, shouldComputeAliasToMakeVisible = true): SymbolVisibilityResult {
6077+
function isEntityNameVisible(entityName: EntityNameOrEntityNameExpression | ElementAccessExpression, enclosingDeclaration: Node, shouldComputeAliasToMakeVisible = true): SymbolVisibilityResult {
60766078
const meaning = getMeaningOfEntityNameReference(entityName);
60776079
const firstIdentifier = getFirstIdentifier(entityName);
60786080
const symbol = resolveName(enclosingDeclaration, firstIdentifier.escapedText, meaning, /*nameNotFoundMessage*/ undefined, /*isUse*/ false);
@@ -8369,9 +8371,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
83698371
}
83708372
}
83718373

8372-
function trackComputedName(accessExpression: EntityNameOrEntityNameExpression, enclosingDeclaration: Node | undefined, context: NodeBuilderContext) {
8374+
function trackComputedName(accessExpression: EntityNameOrEntityNameExpression | ElementAccessExpression, enclosingDeclaration: Node | undefined, context: NodeBuilderContext) {
83738375
if (!context.tracker.canTrackSymbol) return;
8374-
// get symbol of the first identifier of the entityName
8376+
// get symbol of the first identifier of the entityName or element access chain
83758377
const firstIdentifier = getFirstIdentifier(accessExpression);
83768378
const name = resolveName(enclosingDeclaration, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
83778379
if (name) {
@@ -13724,32 +13726,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1372413726
return isLateBindableAccessExpression(expr);
1372513727
}
1372613728

13727-
/**
13728-
* Returns true if the expression is a valid late-bindable access expression.
13729-
* A late-bindable access expression is:
13730-
* - An Identifier
13731-
* - A PropertyAccessExpression where the base is a late-bindable access expression
13732-
* - An ElementAccessExpression with a string/numeric literal key where the base is a late-bindable access expression
13733-
*
13734-
* This supports mixed chains like: obj.a['b'].c['d']
13735-
* Parentheses are skipped to support expressions like: (obj.a)['b']
13736-
*/
13737-
function isLateBindableAccessExpression(node: Node): boolean {
13738-
node = skipParentheses(node);
13739-
13740-
if (isIdentifier(node)) {
13741-
return true;
13742-
}
13743-
// For PropertyAccessExpression, require the name to be an Identifier (not PrivateIdentifier)
13744-
if (isPropertyAccessExpression(node) && isIdentifier(node.name)) {
13745-
return isLateBindableAccessExpression(node.expression);
13746-
}
13747-
if (isElementAccessExpression(node) && isStringOrNumericLiteralLike(skipParentheses(node.argumentExpression))) {
13748-
return isLateBindableAccessExpression(node.expression);
13749-
}
13750-
return false;
13751-
}
13752-
1375313729
function isTypeUsableAsIndexSignature(type: Type): boolean {
1375413730
return isTypeAssignableTo(type, stringNumberSymbolType);
1375513731
}
@@ -51470,9 +51446,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
5147051446
}
5147151447
return result;
5147251448

51473-
function trackComputedName(accessExpression: EntityNameOrEntityNameExpression) {
51449+
function trackComputedName(accessExpression: EntityNameOrEntityNameExpression | ElementAccessExpression) {
5147451450
if (!tracker.trackSymbol) return;
51475-
// get symbol of the first identifier of the entityName
51451+
// get symbol of the first identifier of the entityName or element access chain
5147651452
const firstIdentifier = getFirstIdentifier(accessExpression);
5147751453
const name = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nameNotFoundMessage*/ undefined, /*isUse*/ true);
5147851454
if (name) {

src/compiler/transformers/declarations.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
declarationNameToString,
3333
Diagnostics,
3434
DiagnosticWithLocation,
35+
ElementAccessExpression,
3536
EmitFlags,
3637
EmitHost,
3738
EmitResolver,
@@ -118,6 +119,7 @@ import {
118119
isInternalDeclaration,
119120
isJSDocImportTag,
120121
isJsonSourceFile,
122+
isLateBindableAccessExpression,
121123
isLateVisibilityPaintedStatement,
122124
isLiteralImportTypeNode,
123125
isMappedTypeNode,
@@ -616,8 +618,11 @@ export function transformDeclarations(context: TransformationContext): Transform
616618
if (elem.kind === SyntaxKind.OmittedExpression) {
617619
return elem;
618620
}
619-
if (elem.propertyName && isComputedPropertyName(elem.propertyName) && isEntityNameExpression(elem.propertyName.expression)) {
620-
checkEntityNameVisibility(elem.propertyName.expression, enclosingDeclaration);
621+
if (elem.propertyName && isComputedPropertyName(elem.propertyName)) {
622+
const expr = elem.propertyName.expression;
623+
if (isLateBindableAccessExpression(expr)) {
624+
checkEntityNameVisibility(expr, enclosingDeclaration);
625+
}
621626
}
622627

623628
return factory.updateBindingElement(
@@ -815,7 +820,7 @@ export function transformDeclarations(context: TransformationContext): Transform
815820
|| isMappedTypeNode(node);
816821
}
817822

818-
function checkEntityNameVisibility(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node) {
823+
function checkEntityNameVisibility(entityName: EntityNameOrEntityNameExpression | ElementAccessExpression, enclosingDeclaration: Node) {
819824
const visibilityResult = resolver.isEntityNameVisible(entityName, enclosingDeclaration);
820825
handleSymbolAccessibilityError(visibilityResult);
821826
}
@@ -1012,16 +1017,16 @@ export function transformDeclarations(context: TransformationContext): Transform
10121017
return;
10131018
}
10141019
else if (
1015-
// Type declarations just need to double-check that the input computed name is an entity name expression
1020+
// Type declarations just need to double-check that the input computed name is a late-bindable access expression
10161021
(isInterfaceDeclaration(input.parent) || isTypeLiteralNode(input.parent))
1017-
&& !isEntityNameExpression(input.name.expression)
1022+
&& !isLateBindableAccessExpression(input.name.expression)
10181023
) {
10191024
context.addDiagnostic(createDiagnosticForNode(input, Diagnostics.Computed_properties_must_be_number_or_string_literals_variables_or_dotted_expressions_with_isolatedDeclarations));
10201025
return;
10211026
}
10221027
}
10231028
}
1024-
else if (!resolver.isLateBound(getParseTreeNode(input) as Declaration) || !isEntityNameExpression(input.name.expression)) {
1029+
else if (!resolver.isLateBound(getParseTreeNode(input) as Declaration) || !isLateBindableAccessExpression(input.name.expression)) {
10251030
return;
10261031
}
10271032
}

src/compiler/types.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,11 +1813,19 @@ export interface GeneratedPrivateIdentifier extends PrivateIdentifier {
18131813
}
18141814

18151815
/** @internal */
1816-
// A name that supports late-binding (used in checker)
1816+
// A name that supports late-binding (used in checker).
1817+
// Supports both property access chains (a.b.c) and element access chains (a['b']['c']).
18171818
export interface LateBoundName extends ComputedPropertyName {
1818-
readonly expression: EntityNameExpression;
1819+
readonly expression: EntityNameExpression | ElementAccessExpression;
18191820
}
18201821

1822+
/**
1823+
* An expression that can be used in a late-bindable computed property name.
1824+
* Includes property access chains (a.b.c) and element access chains with literal keys (a['b']['c']).
1825+
* @internal
1826+
*/
1827+
export type LateBindableAccessExpression = EntityNameExpression | ElementAccessExpression;
1828+
18211829
export interface Decorator extends Node {
18221830
readonly kind: SyntaxKind.Decorator;
18231831
readonly parent: NamedDeclaration;
@@ -5917,7 +5925,7 @@ export interface EmitResolver {
59175925
createTypeOfExpression(expr: Expression, enclosingDeclaration: Node, flags: NodeBuilderFlags, internalFlags: InternalNodeBuilderFlags, tracker: SymbolTracker): TypeNode | undefined;
59185926
createLiteralConstValue(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration, tracker: SymbolTracker): Expression;
59195927
isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags | undefined, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult;
5920-
isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node): SymbolVisibilityResult;
5928+
isEntityNameVisible(entityName: EntityNameOrEntityNameExpression | ElementAccessExpression, enclosingDeclaration: Node): SymbolVisibilityResult;
59215929
// Returns the constant value this property access resolves to, or 'undefined' for a non-constant
59225930
getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined;
59235931
getEnumMemberValue(node: EnumMember): EvaluatorResult | undefined;
@@ -10614,7 +10622,7 @@ export interface SyntacticTypeNodeBuilderResolver {
1061410622
getAllAccessorDeclarations(declaration: AccessorDeclaration): AllAccessorDeclarations;
1061510623
requiresAddingImplicitUndefined(declaration: ParameterDeclaration | PropertySignature | JSDocParameterTag | JSDocPropertyTag | PropertyDeclaration, symbol: Symbol | undefined, enclosingDeclaration: Node | undefined): boolean;
1061610624
isDefinitelyReferenceToGlobalSymbolObject(node: Node): boolean;
10617-
isEntityNameVisible(context: SyntacticTypeNodeBuilderContext, entityName: EntityNameOrEntityNameExpression, shouldComputeAliasToMakeVisible?: boolean): SymbolVisibilityResult;
10625+
isEntityNameVisible(context: SyntacticTypeNodeBuilderContext, entityName: EntityNameOrEntityNameExpression | ElementAccessExpression, shouldComputeAliasToMakeVisible?: boolean): SymbolVisibilityResult;
1061810626
serializeExistingTypeNode(context: SyntacticTypeNodeBuilderContext, node: TypeNode, addUndefined?: boolean): TypeNode | undefined;
1061910627
serializeReturnTypeForSignature(context: SyntacticTypeNodeBuilderContext, signatureDeclaration: SignatureDeclaration | JSDocSignature, symbol: Symbol | undefined): TypeNode | undefined;
1062010628
serializeTypeOfExpression(context: SyntacticTypeNodeBuilderContext, expr: Expression): TypeNode;

src/compiler/utilities.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ import {
412412
LanguageVariant,
413413
last,
414414
lastOrUndefined,
415+
LateBindableAccessExpression,
415416
LateVisibilityPaintedStatement,
416417
length,
417418
libMap,
@@ -7454,8 +7455,13 @@ export function isEntityNameExpression(node: Node): node is EntityNameExpression
74547455
return node.kind === SyntaxKind.Identifier || isPropertyAccessEntityNameExpression(node);
74557456
}
74567457

7457-
/** @internal */
7458-
export function getFirstIdentifier(node: EntityNameOrEntityNameExpression): Identifier {
7458+
/**
7459+
* Gets the first identifier in a name chain.
7460+
* Supports qualified names (a.b.c), property access expressions (a.b.c),
7461+
* and element access expressions (a['b']['c']).
7462+
* @internal
7463+
*/
7464+
export function getFirstIdentifier(node: EntityNameOrEntityNameExpression | ElementAccessExpression): Identifier {
74597465
switch (node.kind) {
74607466
case SyntaxKind.Identifier:
74617467
return node;
@@ -7466,14 +7472,43 @@ export function getFirstIdentifier(node: EntityNameOrEntityNameExpression): Iden
74667472
while (node.kind !== SyntaxKind.Identifier);
74677473
return node;
74687474
case SyntaxKind.PropertyAccessExpression:
7475+
case SyntaxKind.ElementAccessExpression:
7476+
let expr: Expression = node;
74697477
do {
7470-
node = node.expression;
7478+
expr = (expr as PropertyAccessExpression | ElementAccessExpression).expression;
74717479
}
7472-
while (node.kind !== SyntaxKind.Identifier);
7473-
return node;
7480+
while (expr.kind !== SyntaxKind.Identifier);
7481+
return expr as Identifier;
74747482
}
74757483
}
74767484

7485+
/**
7486+
* Returns true if the expression is a valid late-bindable access expression.
7487+
* A late-bindable access expression is:
7488+
* - An Identifier
7489+
* - A PropertyAccessExpression where the base is a late-bindable access expression
7490+
* - An ElementAccessExpression with a string/numeric literal key where the base is a late-bindable access expression
7491+
*
7492+
* This supports mixed chains like: obj.a['b'].c['d']
7493+
* Parentheses are skipped to support expressions like: (obj.a)['b']
7494+
* @internal
7495+
*/
7496+
export function isLateBindableAccessExpression(node: Node): node is LateBindableAccessExpression {
7497+
node = skipParentheses(node as Expression);
7498+
7499+
if (isIdentifier(node)) {
7500+
return true;
7501+
}
7502+
// For PropertyAccessExpression, require the name to be an Identifier (not PrivateIdentifier)
7503+
if (isPropertyAccessExpression(node) && isIdentifier(node.name)) {
7504+
return isLateBindableAccessExpression(node.expression);
7505+
}
7506+
if (isElementAccessExpression(node) && isStringOrNumericLiteralLike(skipParentheses(node.argumentExpression))) {
7507+
return isLateBindableAccessExpression(node.expression);
7508+
}
7509+
return false;
7510+
}
7511+
74777512
/** @internal */
74787513
export function isDottedName(node: Expression): boolean {
74797514
return node.kind === SyntaxKind.Identifier
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @strict: true
2+
// @declaration: true
3+
4+
// Test that export scenario with enum bracket notation doesn't crash
5+
// This tests the trackComputedName -> getFirstIdentifier path
6+
7+
enum Type {
8+
Foo = 'foo',
9+
'3x14' = '3x14'
10+
}
11+
12+
// Export interface with bracket notation computed property
13+
// This should trigger the tracker path
14+
export interface TypeMap {
15+
[Type['3x14']]: number;
16+
}
17+
18+
export type TypeMap2 = {
19+
[Type['3x14']]: string;
20+
}

0 commit comments

Comments
 (0)