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;
+}`
+});