Skip to content

Commit 2136bef

Browse files
authored
fix(54694): Class incorrectly implements interface generated with template string literal mapped type (#54715)
1 parent 5128e06 commit 2136bef

File tree

5 files changed

+67
-23
lines changed

5 files changed

+67
-23
lines changed

src/compiler/checker.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ import {
335335
getParseTreeNode,
336336
getPropertyAssignmentAliasLikeExpression,
337337
getPropertyNameForPropertyNameNode,
338+
getPropertyNameFromType,
338339
getResolutionDiagnostic,
339340
getResolutionModeOverrideForClause,
340341
getResolvedExternalModuleName,
@@ -729,6 +730,7 @@ import {
729730
isTypeQueryNode,
730731
isTypeReferenceNode,
731732
isTypeReferenceType,
733+
isTypeUsableAsPropertyName,
732734
isUMDExportSymbol,
733735
isValidBigIntString,
734736
isValidESSymbolDeclaration,
@@ -12286,13 +12288,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1228612288
return type as InterfaceTypeWithDeclaredMembers;
1228712289
}
1228812290

12289-
/**
12290-
* Indicates whether a type can be used as a property name.
12291-
*/
12292-
function isTypeUsableAsPropertyName(type: Type): type is StringLiteralType | NumberLiteralType | UniqueESSymbolType {
12293-
return !!(type.flags & TypeFlags.StringOrNumberLiteralOrUnique);
12294-
}
12295-
1229612291
/**
1229712292
* Indicates whether a declaration name is definitely late-bindable.
1229812293
* A declaration name is only late-bindable if:
@@ -12338,19 +12333,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1233812333
return isDynamicName(node) && !isLateBindableName(node);
1233912334
}
1234012335

12341-
/**
12342-
* Gets the symbolic name for a member from its type.
12343-
*/
12344-
function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String {
12345-
if (type.flags & TypeFlags.UniqueESSymbol) {
12346-
return (type as UniqueESSymbolType).escapedName;
12347-
}
12348-
if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
12349-
return escapeLeadingUnderscores("" + (type as StringLiteralType | NumberLiteralType).value);
12350-
}
12351-
return Debug.fail();
12352-
}
12353-
1235412336
/**
1235512337
* Adds a declaration to a late-bound dynamic member. This performs the same function for
1235612338
* late-bound members that `addDeclarationToSymbol` in binder.ts performs for early-bound

src/compiler/utilities.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ import {
416416
noop,
417417
normalizePath,
418418
NoSubstitutionTemplateLiteral,
419+
NumberLiteralType,
419420
NumericLiteral,
420421
ObjectFlags,
421422
ObjectFlagsType,
@@ -494,6 +495,7 @@ import {
494495
stringContains,
495496
StringLiteral,
496497
StringLiteralLike,
498+
StringLiteralType,
497499
stringToToken,
498500
SuperCall,
499501
SuperExpression,
@@ -544,6 +546,7 @@ import {
544546
TypeReferenceNode,
545547
unescapeLeadingUnderscores,
546548
UnionOrIntersectionTypeNode,
549+
UniqueESSymbolType,
547550
UserPreferences,
548551
ValidImportTypeNode,
549552
VariableDeclaration,
@@ -10313,3 +10316,25 @@ export function getTextOfJsxNamespacedName(node: JsxNamespacedName) {
1031310316
export function intrinsicTagNameToString(node: Identifier | JsxNamespacedName) {
1031410317
return isIdentifier(node) ? idText(node) : getTextOfJsxNamespacedName(node);
1031510318
}
10319+
10320+
/**
10321+
* Indicates whether a type can be used as a property name.
10322+
* @internal
10323+
*/
10324+
export function isTypeUsableAsPropertyName(type: Type): type is StringLiteralType | NumberLiteralType | UniqueESSymbolType {
10325+
return !!(type.flags & TypeFlags.StringOrNumberLiteralOrUnique);
10326+
}
10327+
10328+
/**
10329+
* Gets the symbolic name for a member from its type.
10330+
* @internal
10331+
*/
10332+
export function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String {
10333+
if (type.flags & TypeFlags.UniqueESSymbol) {
10334+
return (type as UniqueESSymbolType).escapedName;
10335+
}
10336+
if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
10337+
return escapeLeadingUnderscores("" + (type as StringLiteralType | NumberLiteralType).value);
10338+
}
10339+
return Debug.fail();
10340+
}

src/services/codefixes/helpers.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,32 @@ import {
66
Block,
77
CallExpression,
88
CharacterCodes,
9+
CheckFlags,
910
ClassLikeDeclaration,
1011
CodeFixContextBase,
1112
combine,
1213
Debug,
14+
Declaration,
1315
Diagnostics,
1416
emptyArray,
1517
EntityName,
1618
Expression,
1719
factory,
1820
find,
21+
firstOrUndefined,
1922
flatMap,
2023
FunctionDeclaration,
2124
FunctionExpression,
2225
GetAccessorDeclaration,
2326
getAllAccessorDeclarations,
27+
getCheckFlags,
2428
getEffectiveModifierFlags,
2529
getEmitScriptTarget,
2630
getFirstIdentifier,
2731
getModuleSpecifierResolverHost,
2832
getNameForExportedSymbol,
2933
getNameOfDeclaration,
34+
getPropertyNameFromType,
3035
getQuotePreference,
3136
getSetAccessorValueParameter,
3237
getSynthesizedDeepClone,
@@ -52,6 +57,7 @@ import {
5257
isSetAccessorDeclaration,
5358
isStringLiteral,
5459
isTypeNode,
60+
isTypeUsableAsPropertyName,
5561
isYieldExpression,
5662
LanguageServiceHost,
5763
length,
@@ -91,13 +97,15 @@ import {
9197
textChanges,
9298
TextSpan,
9399
textSpanEnd,
100+
TransientSymbol,
94101
tryCast,
95102
TsConfigSourceFile,
96103
Type,
97104
TypeChecker,
98105
TypeFlags,
99106
TypeNode,
100107
TypeParameterDeclaration,
108+
unescapeLeadingUnderscores,
101109
UnionType,
102110
UserPreferences,
103111
visitEachChild,
@@ -174,7 +182,7 @@ export function addNewNodeForMemberSymbol(
174182
isAmbient = false,
175183
): void {
176184
const declarations = symbol.getDeclarations();
177-
const declaration = declarations?.[0];
185+
const declaration = firstOrUndefined(declarations);
178186
const checker = context.program.getTypeChecker();
179187
const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions());
180188

@@ -193,7 +201,7 @@ export function addNewNodeForMemberSymbol(
193201
* In such cases, we assume the declaration to be a `PropertySignature`.
194202
*/
195203
const kind = declaration?.kind ?? SyntaxKind.PropertySignature;
196-
const declarationName = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName;
204+
const declarationName = createDeclarationName(symbol, declaration);
197205
const effectiveModifierFlags = declaration ? getEffectiveModifierFlags(declaration) : ModifierFlags.None;
198206
let modifierFlags = effectiveModifierFlags & ModifierFlags.Static;
199207
modifierFlags |=
@@ -310,7 +318,6 @@ export function addNewNodeForMemberSymbol(
310318
if (method) addClassElement(method);
311319
}
312320

313-
314321
function createModifiers(): NodeArray<Modifier> | undefined {
315322
let modifiers: Modifier[] | undefined;
316323

@@ -344,6 +351,16 @@ export function addNewNodeForMemberSymbol(
344351
function createTypeNode(typeNode: TypeNode | undefined) {
345352
return getSynthesizedDeepClone(typeNode, /*includeTrivia*/ false);
346353
}
354+
355+
function createDeclarationName(symbol: Symbol, declaration: Declaration | undefined): PropertyName {
356+
if (getCheckFlags(symbol) & CheckFlags.Mapped) {
357+
const nameType = (symbol as TransientSymbol).links.nameType;
358+
if (nameType && isTypeUsableAsPropertyName(nameType)) {
359+
return factory.createIdentifier(unescapeLeadingUnderscores(getPropertyNameFromType(nameType)));
360+
}
361+
}
362+
return getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName;
363+
}
347364
}
348365

349366
/** @internal */
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////type ListenerTemplate<T, S extends string, I extends string = "${1}"> = {
4+
//// [K in keyof T as K extends string
5+
//// ? S extends `${infer F}${I}${infer R}` ? `${F}${K}${R}` : K : K]
6+
//// : (listener: (payload: T[K]) => void) => void;
7+
////};
8+
////type ListenActionable<E> = ListenerTemplate<E, "add*Listener" | "remove*Listener", "*">;
9+
////type ClickEventSupport = ListenActionable<{ Click: 'some-click-event-payload' }>;
10+
////
11+
////[|class C implements ClickEventSupport { }|]
12+
13+
verify.codeFix({
14+
description: "Implement interface 'ClickEventSupport'",
15+
newRangeContent:
16+
`class C implements ClickEventSupport {
17+
addClickListener: (listener: (payload: "some-click-event-payload") => void) => void;
18+
removeClickListener: (listener: (payload: "some-click-event-payload") => void) => void;
19+
}`,
20+
});

0 commit comments

Comments
 (0)