diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index e50c7e35a95fa..721870f640743 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -5332,6 +5332,10 @@ "category": "Message", "code": 95002 }, + "Convert '{0}' to '{1} in {0}'": { + "category": "Message", + "code": 95003 + }, "Extract to {0} in {1}": { "category": "Message", "code": 95004 @@ -5400,6 +5404,10 @@ "category": "Message", "code": 95020 }, + "Convert all type literals to mapped type": { + "category": "Message", + "code": 95021 + }, "Add all missing members": { "category": "Message", "code": 95022 diff --git a/src/services/codefixes/convertLiteralTypeToMappedType.ts b/src/services/codefixes/convertLiteralTypeToMappedType.ts new file mode 100644 index 0000000000000..715c666a95387 --- /dev/null +++ b/src/services/codefixes/convertLiteralTypeToMappedType.ts @@ -0,0 +1,53 @@ +/* @internal */ +namespace ts.codefix { + const fixId = "convertLiteralTypeToMappedType"; + const errorCodes = [Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here_Did_you_mean_to_use_1_in_0.code]; + + registerCodeFix({ + errorCodes, + getCodeActions: context => { + const { sourceFile, span } = context; + const info = getInfo(sourceFile, span.start); + if (!info) { + return undefined; + } + const { name, constraint } = info; + const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, info)); + return [createCodeFixAction(fixId, changes, [Diagnostics.Convert_0_to_1_in_0, constraint, name], fixId, Diagnostics.Convert_all_type_literals_to_mapped_type)]; + }, + fixIds: [fixId], + getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => { + const info = getInfo(diag.file, diag.start); + if (info) { + doChange(changes, diag.file, info); + } + }) + }); + + interface Info { + container: TypeLiteralNode, + typeNode: TypeNode | undefined; + constraint: string; + name: string; + } + + function getInfo(sourceFile: SourceFile, pos: number): Info | undefined { + const token = getTokenAtPosition(sourceFile, pos); + if (isIdentifier(token)) { + const propertySignature = cast(token.parent.parent, isPropertySignature); + const propertyName = token.getText(sourceFile); + return { + container: cast(propertySignature.parent, isTypeLiteralNode), + typeNode: propertySignature.type, + constraint: propertyName, + name: propertyName === "K" ? "P" : "K", + }; + } + return undefined; + } + + function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, { container, typeNode, constraint, name }: Info): void { + changes.replaceNode(sourceFile, container, factory.createMappedTypeNode(/*readonlyToken*/ undefined, + factory.createTypeParameterDeclaration(name, factory.createTypeReferenceNode(constraint)), /*questionToken*/ undefined, typeNode)); + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index cc24ac7f9c405..7bcfb701dfed1 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -61,6 +61,7 @@ "codefixes/correctQualifiedNameToIndexedAccessType.ts", "codefixes/convertToTypeOnlyExport.ts", "codefixes/convertToTypeOnlyImport.ts", + "codefixes/convertLiteralTypeToMappedType.ts", "codefixes/fixClassIncorrectlyImplementsInterface.ts", "codefixes/importFixes.ts", "codefixes/fixImplicitThis.ts", diff --git a/tests/cases/fourslash/convertLiteralTypeToMappedType1.ts b/tests/cases/fourslash/convertLiteralTypeToMappedType1.ts new file mode 100644 index 0000000000000..4da4b0e7a669c --- /dev/null +++ b/tests/cases/fourslash/convertLiteralTypeToMappedType1.ts @@ -0,0 +1,16 @@ +/// + +////type K = number | string; +////type T = { +//// [K]: number; +////} + +verify.codeFix({ + description: [ts.Diagnostics.Convert_0_to_1_in_0.message, "K", "P"], + index: 0, + newFileContent: +`type K = number | string; +type T = { + [P in K]: number; +}` +}); diff --git a/tests/cases/fourslash/convertLiteralTypeToMappedType2.ts b/tests/cases/fourslash/convertLiteralTypeToMappedType2.ts new file mode 100644 index 0000000000000..94b2a00a0056e --- /dev/null +++ b/tests/cases/fourslash/convertLiteralTypeToMappedType2.ts @@ -0,0 +1,16 @@ +/// + +////type Keys = number | string; +////type T = { +//// [Keys]: number; +////} + +verify.codeFix({ + description: [ts.Diagnostics.Convert_0_to_1_in_0.message, "Keys", "K"], + index: 0, + newFileContent: +`type Keys = number | string; +type T = { + [K in Keys]: number; +}` +}); diff --git a/tests/cases/fourslash/convertLiteralTypeToMappedType3.ts b/tests/cases/fourslash/convertLiteralTypeToMappedType3.ts new file mode 100644 index 0000000000000..bade55c0bc5f9 --- /dev/null +++ b/tests/cases/fourslash/convertLiteralTypeToMappedType3.ts @@ -0,0 +1,24 @@ +/// + +////type K1 = number | string; +////type T1 = { +//// [K1]: number; +////} +////type K2 = number | string; +////type T2 = { +//// [K2]: number; +////} + +verify.codeFixAll({ + fixAllDescription: ts.Diagnostics.Convert_all_type_literals_to_mapped_type.message, + fixId: 'convertLiteralTypeToMappedType', + newFileContent: +`type K1 = number | string; +type T1 = { + [K in K1]: number; +} +type K2 = number | string; +type T2 = { + [K in K2]: number; +}` +});