fix(54694): Class incorrectly implements interface generated with template string literal mapped type (#54715)

This commit is contained in:
Oleksandr T 2023-07-20 23:51:32 +03:00 committed by GitHub
parent 5128e06a9d
commit 2136bef652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 67 additions and 23 deletions

View File

@ -335,6 +335,7 @@ import {
getParseTreeNode,
getPropertyAssignmentAliasLikeExpression,
getPropertyNameForPropertyNameNode,
getPropertyNameFromType,
getResolutionDiagnostic,
getResolutionModeOverrideForClause,
getResolvedExternalModuleName,
@ -729,6 +730,7 @@ import {
isTypeQueryNode,
isTypeReferenceNode,
isTypeReferenceType,
isTypeUsableAsPropertyName,
isUMDExportSymbol,
isValidBigIntString,
isValidESSymbolDeclaration,
@ -12286,13 +12288,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type as InterfaceTypeWithDeclaredMembers;
}
/**
* Indicates whether a type can be used as a property name.
*/
function isTypeUsableAsPropertyName(type: Type): type is StringLiteralType | NumberLiteralType | UniqueESSymbolType {
return !!(type.flags & TypeFlags.StringOrNumberLiteralOrUnique);
}
/**
* Indicates whether a declaration name is definitely late-bindable.
* A declaration name is only late-bindable if:
@ -12338,19 +12333,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return isDynamicName(node) && !isLateBindableName(node);
}
/**
* Gets the symbolic name for a member from its type.
*/
function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String {
if (type.flags & TypeFlags.UniqueESSymbol) {
return (type as UniqueESSymbolType).escapedName;
}
if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
return escapeLeadingUnderscores("" + (type as StringLiteralType | NumberLiteralType).value);
}
return Debug.fail();
}
/**
* Adds a declaration to a late-bound dynamic member. This performs the same function for
* late-bound members that `addDeclarationToSymbol` in binder.ts performs for early-bound

View File

@ -416,6 +416,7 @@ import {
noop,
normalizePath,
NoSubstitutionTemplateLiteral,
NumberLiteralType,
NumericLiteral,
ObjectFlags,
ObjectFlagsType,
@ -494,6 +495,7 @@ import {
stringContains,
StringLiteral,
StringLiteralLike,
StringLiteralType,
stringToToken,
SuperCall,
SuperExpression,
@ -544,6 +546,7 @@ import {
TypeReferenceNode,
unescapeLeadingUnderscores,
UnionOrIntersectionTypeNode,
UniqueESSymbolType,
UserPreferences,
ValidImportTypeNode,
VariableDeclaration,
@ -10313,3 +10316,25 @@ export function getTextOfJsxNamespacedName(node: JsxNamespacedName) {
export function intrinsicTagNameToString(node: Identifier | JsxNamespacedName) {
return isIdentifier(node) ? idText(node) : getTextOfJsxNamespacedName(node);
}
/**
* Indicates whether a type can be used as a property name.
* @internal
*/
export function isTypeUsableAsPropertyName(type: Type): type is StringLiteralType | NumberLiteralType | UniqueESSymbolType {
return !!(type.flags & TypeFlags.StringOrNumberLiteralOrUnique);
}
/**
* Gets the symbolic name for a member from its type.
* @internal
*/
export function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String {
if (type.flags & TypeFlags.UniqueESSymbol) {
return (type as UniqueESSymbolType).escapedName;
}
if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) {
return escapeLeadingUnderscores("" + (type as StringLiteralType | NumberLiteralType).value);
}
return Debug.fail();
}

View File

@ -6,27 +6,32 @@ import {
Block,
CallExpression,
CharacterCodes,
CheckFlags,
ClassLikeDeclaration,
CodeFixContextBase,
combine,
Debug,
Declaration,
Diagnostics,
emptyArray,
EntityName,
Expression,
factory,
find,
firstOrUndefined,
flatMap,
FunctionDeclaration,
FunctionExpression,
GetAccessorDeclaration,
getAllAccessorDeclarations,
getCheckFlags,
getEffectiveModifierFlags,
getEmitScriptTarget,
getFirstIdentifier,
getModuleSpecifierResolverHost,
getNameForExportedSymbol,
getNameOfDeclaration,
getPropertyNameFromType,
getQuotePreference,
getSetAccessorValueParameter,
getSynthesizedDeepClone,
@ -52,6 +57,7 @@ import {
isSetAccessorDeclaration,
isStringLiteral,
isTypeNode,
isTypeUsableAsPropertyName,
isYieldExpression,
LanguageServiceHost,
length,
@ -91,6 +97,7 @@ import {
textChanges,
TextSpan,
textSpanEnd,
TransientSymbol,
tryCast,
TsConfigSourceFile,
Type,
@ -98,6 +105,7 @@ import {
TypeFlags,
TypeNode,
TypeParameterDeclaration,
unescapeLeadingUnderscores,
UnionType,
UserPreferences,
visitEachChild,
@ -174,7 +182,7 @@ export function addNewNodeForMemberSymbol(
isAmbient = false,
): void {
const declarations = symbol.getDeclarations();
const declaration = declarations?.[0];
const declaration = firstOrUndefined(declarations);
const checker = context.program.getTypeChecker();
const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions());
@ -193,7 +201,7 @@ export function addNewNodeForMemberSymbol(
* In such cases, we assume the declaration to be a `PropertySignature`.
*/
const kind = declaration?.kind ?? SyntaxKind.PropertySignature;
const declarationName = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName;
const declarationName = createDeclarationName(symbol, declaration);
const effectiveModifierFlags = declaration ? getEffectiveModifierFlags(declaration) : ModifierFlags.None;
let modifierFlags = effectiveModifierFlags & ModifierFlags.Static;
modifierFlags |=
@ -310,7 +318,6 @@ export function addNewNodeForMemberSymbol(
if (method) addClassElement(method);
}
function createModifiers(): NodeArray<Modifier> | undefined {
let modifiers: Modifier[] | undefined;
@ -344,6 +351,16 @@ export function addNewNodeForMemberSymbol(
function createTypeNode(typeNode: TypeNode | undefined) {
return getSynthesizedDeepClone(typeNode, /*includeTrivia*/ false);
}
function createDeclarationName(symbol: Symbol, declaration: Declaration | undefined): PropertyName {
if (getCheckFlags(symbol) & CheckFlags.Mapped) {
const nameType = (symbol as TransientSymbol).links.nameType;
if (nameType && isTypeUsableAsPropertyName(nameType)) {
return factory.createIdentifier(unescapeLeadingUnderscores(getPropertyNameFromType(nameType)));
}
}
return getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName;
}
}
/** @internal */

View File

@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />
////type ListenerTemplate<T, S extends string, I extends string = "${1}"> = {
//// [K in keyof T as K extends string
//// ? S extends `${infer F}${I}${infer R}` ? `${F}${K}${R}` : K : K]
//// : (listener: (payload: T[K]) => void) => void;
////};
////type ListenActionable<E> = ListenerTemplate<E, "add*Listener" | "remove*Listener", "*">;
////type ClickEventSupport = ListenActionable<{ Click: 'some-click-event-payload' }>;
////
////[|class C implements ClickEventSupport { }|]
verify.codeFix({
description: "Implement interface 'ClickEventSupport'",
newRangeContent:
`class C implements ClickEventSupport {
addClickListener: (listener: (payload: "some-click-event-payload") => void) => void;
removeClickListener: (listener: (payload: "some-click-event-payload") => void) => void;
}`,
});