2
2
namespace ts . refactor {
3
3
const refactorName = "Extract type" ;
4
4
const extractToTypeAlias = "Extract to type alias" ;
5
+ const extractToInterface = "Extract to interface" ;
5
6
const extractToTypeDef = "Extract to typedef" ;
6
7
registerRefactor ( refactorName , {
7
8
getAvailableActions ( context ) : ReadonlyArray < ApplicableRefactorInfo > {
@@ -11,31 +12,51 @@ namespace ts.refactor {
11
12
return [ {
12
13
name : refactorName ,
13
14
description : getLocaleSpecificMessage ( Diagnostics . Extract_type ) ,
14
- actions : [ info . isJS ? {
15
+ actions : info . isJS ? [ {
15
16
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
+ } )
19
22
} ] ;
20
23
} ,
21
24
getEditsForAction ( context , actionName ) : RefactorEditInfo {
22
- Debug . assert ( actionName === extractToTypeAlias || actionName === extractToTypeDef , "Unexpected action name" ) ;
23
25
const { file } = context ;
24
26
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" ) ;
26
27
27
28
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
+ } ) ;
31
44
32
45
const renameFilename = file . fileName ;
33
46
const renameLocation = getRenameLocation ( edits , renameFilename , name , /*preferLastLocation*/ false ) ;
34
47
return { edits, renameFilename, renameLocation } ;
35
48
}
36
49
} ) ;
37
50
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 ;
39
60
40
61
function getRangeToExtract ( context : RefactorContext ) : Info | undefined {
41
62
const { file, startPosition } = context ;
@@ -51,7 +72,32 @@ namespace ts.refactor {
51
72
const typeParameters = collectTypeParameters ( checker , selection , firstStatement , file ) ;
52
73
if ( ! typeParameters ) return undefined ;
53
74
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 ;
55
101
}
56
102
57
103
function isStatementAndHasJSDoc ( n : Node ) : n is ( Statement & HasJSDoc ) {
@@ -107,7 +153,9 @@ namespace ts.refactor {
107
153
}
108
154
}
109
155
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
+
111
159
const newTypeNode = createTypeAliasDeclaration (
112
160
/* decorators */ undefined ,
113
161
/* modifiers */ undefined ,
@@ -119,7 +167,24 @@ namespace ts.refactor {
119
167
changes . replaceNode ( file , selection , createTypeReferenceNode ( name , typeParameters . map ( id => createTypeReferenceNode ( id . name , /* typeArguments */ undefined ) ) ) ) ;
120
168
}
121
169
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
+
123
188
const node = < JSDocTypedefTag > createNode ( SyntaxKind . JSDocTypedefTag ) ;
124
189
node . tagName = createIdentifier ( "typedef" ) ; // TODO: jsdoc factory https://github.com/Microsoft/TypeScript/pull/29539
125
190
node . fullName = createIdentifier ( name ) ;
0 commit comments