22namespace 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 ) ;
0 commit comments