Skip to content

Commit af9ca21

Browse files
Kingwlandrewbranch
authored andcommitted
add support for extract as interface (#31644)
* add support for extract as interface * fix action assert * Donot provide convert to interface if duplicate member
1 parent 5d36aab commit af9ca21

11 files changed

+172
-13
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5120,6 +5120,10 @@
51205120
"category": "Message",
51215121
"code": 95089
51225122
},
5123+
"Extract to interface": {
5124+
"category": "Message",
5125+
"code": 95090
5126+
},
51235127

51245128
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
51255129
"category": "Error",

src/services/refactors/extractType.ts

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace ts.refactor {
33
const refactorName = "Extract type";
44
const extractToTypeAlias = "Extract to type alias";
5+
const extractToInterface = "Extract to interface";
56
const extractToTypeDef = "Extract to typedef";
67
registerRefactor(refactorName, {
78
getAvailableActions(context): ReadonlyArray<ApplicableRefactorInfo> {
@@ -11,31 +12,51 @@ namespace ts.refactor {
1112
return [{
1213
name: refactorName,
1314
description: getLocaleSpecificMessage(Diagnostics.Extract_type),
14-
actions: [info.isJS ? {
15+
actions: info.isJS ? [{
1516
name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef)
16-
} : {
17-
name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias)
18-
}]
17+
}] : append([{
18+
name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias)
19+
}], info.typeElements && {
20+
name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface)
21+
})
1922
}];
2023
},
2124
getEditsForAction(context, actionName): RefactorEditInfo {
22-
Debug.assert(actionName === extractToTypeAlias || actionName === extractToTypeDef, "Unexpected action name");
2325
const { file } = context;
2426
const info = Debug.assertDefined(getRangeToExtract(context), "Expected to find a range to extract");
25-
Debug.assert(actionName === extractToTypeAlias && !info.isJS || actionName === extractToTypeDef && info.isJS, "Invalid actionName/JS combo");
2627

2728
const name = getUniqueName("NewType", file);
28-
const edits = textChanges.ChangeTracker.with(context, changes => info.isJS ?
29-
doTypedefChange(changes, file, name, info.firstStatement, info.selection, info.typeParameters) :
30-
doTypeAliasChange(changes, file, name, info.firstStatement, info.selection, info.typeParameters));
29+
const edits = textChanges.ChangeTracker.with(context, changes => {
30+
switch (actionName) {
31+
case extractToTypeAlias:
32+
Debug.assert(!info.isJS, "Invalid actionName/JS combo");
33+
return doTypeAliasChange(changes, file, name, info);
34+
case extractToTypeDef:
35+
Debug.assert(info.isJS, "Invalid actionName/JS combo");
36+
return doTypedefChange(changes, file, name, info);
37+
case extractToInterface:
38+
Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo");
39+
return doInterfaceChange(changes, file, name, info as InterfaceInfo);
40+
default:
41+
Debug.fail("Unexpected action name");
42+
}
43+
});
3144

3245
const renameFilename = file.fileName;
3346
const renameLocation = getRenameLocation(edits, renameFilename, name, /*preferLastLocation*/ false);
3447
return { edits, renameFilename, renameLocation };
3548
}
3649
});
3750

38-
interface Info { isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: ReadonlyArray<TypeParameterDeclaration>; }
51+
interface TypeAliasInfo {
52+
isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: ReadonlyArray<TypeParameterDeclaration>; typeElements?: ReadonlyArray<TypeElement>;
53+
}
54+
55+
interface InterfaceInfo {
56+
isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: ReadonlyArray<TypeParameterDeclaration>; typeElements: ReadonlyArray<TypeElement>;
57+
}
58+
59+
type Info = TypeAliasInfo | InterfaceInfo;
3960

4061
function getRangeToExtract(context: RefactorContext): Info | undefined {
4162
const { file, startPosition } = context;
@@ -51,7 +72,32 @@ namespace ts.refactor {
5172
const typeParameters = collectTypeParameters(checker, selection, firstStatement, file);
5273
if (!typeParameters) return undefined;
5374

54-
return { isJS, selection, firstStatement, typeParameters };
75+
const typeElements = flattenTypeLiteralNodeReference(checker, selection);
76+
return { isJS, selection, firstStatement, typeParameters, typeElements };
77+
}
78+
79+
function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode | undefined): ReadonlyArray<TypeElement> | undefined {
80+
if (!node) return undefined;
81+
if (isIntersectionTypeNode(node)) {
82+
const result: TypeElement[] = [];
83+
const seen = createMap<true>();
84+
for (const type of node.types) {
85+
const flattenedTypeMembers = flattenTypeLiteralNodeReference(checker, type);
86+
if (!flattenedTypeMembers || !flattenedTypeMembers.every(type => type.name && addToSeen(seen, getNameFromPropertyName(type.name) as string))) {
87+
return undefined;
88+
}
89+
90+
addRange(result, flattenedTypeMembers);
91+
}
92+
return result;
93+
}
94+
else if (isParenthesizedTypeNode(node)) {
95+
return flattenTypeLiteralNodeReference(checker, node.type);
96+
}
97+
else if (isTypeLiteralNode(node)) {
98+
return node.members;
99+
}
100+
return undefined;
55101
}
56102

57103
function isStatementAndHasJSDoc(n: Node): n is (Statement & HasJSDoc) {
@@ -107,7 +153,9 @@ namespace ts.refactor {
107153
}
108154
}
109155

110-
function doTypeAliasChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, firstStatement: Statement, selection: TypeNode, typeParameters: ReadonlyArray<TypeParameterDeclaration>) {
156+
function doTypeAliasChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: TypeAliasInfo) {
157+
const { firstStatement, selection, typeParameters } = info;
158+
111159
const newTypeNode = createTypeAliasDeclaration(
112160
/* decorators */ undefined,
113161
/* modifiers */ undefined,
@@ -119,7 +167,24 @@ namespace ts.refactor {
119167
changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined))));
120168
}
121169

122-
function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, firstStatement: Statement, selection: TypeNode, typeParameters: ReadonlyArray<TypeParameterDeclaration>) {
170+
function doInterfaceChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: InterfaceInfo) {
171+
const { firstStatement, selection, typeParameters, typeElements } = info;
172+
173+
const newTypeNode = createInterfaceDeclaration(
174+
/* decorators */ undefined,
175+
/* modifiers */ undefined,
176+
name,
177+
typeParameters,
178+
/* heritageClauses */ undefined,
179+
typeElements
180+
);
181+
changes.insertNodeBefore(file, firstStatement, newTypeNode, /* blankLineBetween */ true);
182+
changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined))));
183+
}
184+
185+
function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: Info) {
186+
const { firstStatement, selection, typeParameters } = info;
187+
123188
const node = <JSDocTypedefTag>createNode(SyntaxKind.JSDocTypedefTag);
124189
node.tagName = createIdentifier("typedef"); // TODO: jsdoc factory https://github.com/Microsoft/TypeScript/pull/29539
125190
node.fullName = createIdentifier(name);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// function foo(a: /*a*/{ a: number | string, b: string }/*b*/) { }
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to interface",
9+
actionDescription: "Extract to interface",
10+
newContent: `interface /*RENAME*/NewType {
11+
a: number | string;
12+
b: string;
13+
}
14+
15+
function foo(a: NewType) { }`,
16+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// function foo(a: /*a*/{ a: number | string, b: string } & { c: string } & { d: boolean }/*b*/) { }
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to interface",
9+
actionDescription: "Extract to interface",
10+
newContent: `interface /*RENAME*/NewType {
11+
a: number | string;
12+
b: string;
13+
c: string;
14+
d: boolean;
15+
}
16+
17+
function foo(a: NewType) { }`,
18+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type T = { c: string }
4+
//// function foo(a: /*a*/{ a: number | string, b: string } & T/*b*/) { }
5+
6+
goTo.select("a", "b");
7+
verify.not.refactorAvailable("Extract type", "Extract to interface")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type T = { c: string } & { d: boolean }
4+
//// function foo(a: /*a*/{ a: number | string, b: string } & T/*b*/) { }
5+
6+
goTo.select("a", "b");
7+
verify.not.refactorAvailable("Extract type", "Extract to interface")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type T = { c: string } & Record<string, string>
4+
//// function foo(a: /*a*/{ a: number | string, b: string } & T/*b*/) { }
5+
6+
goTo.select("a", "b");
7+
verify.not.refactorAvailable("Extract type", "Extract to interface")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// type T = { c: string }
4+
//// function foo(a: /*a*/T/*b*/) { }
5+
6+
goTo.select("a", "b");
7+
verify.not.refactorAvailable("Extract type", "Extract to interface")
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// function foo<U>(a: /*a*/{ a: string } & { b: U }/*b*/) { }
4+
5+
goTo.select("a", "b");
6+
edit.applyRefactor({
7+
refactorName: "Extract type",
8+
actionName: "Extract to interface",
9+
actionDescription: "Extract to interface",
10+
newContent: `interface /*RENAME*/NewType<U> {
11+
a: string;
12+
b: U;
13+
}
14+
15+
function foo<U>(a: NewType<U>) { }`,
16+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// function foo(a: /*a*/{ a: number | string, b: string } & { b: string } & { d: boolean }/*b*/) { }
4+
5+
goTo.select("a", "b");
6+
verify.not.refactorAvailable("Extract type", "Extract to interface")
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// function foo(a: /*a*/{ a: number | string, b: string } & { b: number } & { d: boolean }/*b*/) { }
4+
5+
goTo.select("a", "b");
6+
verify.not.refactorAvailable("Extract type", "Extract to interface")

0 commit comments

Comments
 (0)