1
1
import * as ts from 'typescript' ;
2
+ import * as _ from 'lodash' ;
2
3
3
4
import * as helpers from '../helpers' ;
4
5
@@ -13,8 +14,8 @@ import * as helpers from '../helpers';
13
14
* type Foo = {foo: string; bar: number;}
14
15
*/
15
16
export function collapseIntersectionInterfacesTransformFactoryFactory (
16
- typeChecker : ts . TypeChecker ,
17
- ) : ts . TransformerFactory < ts . SourceFile > {
17
+ typeChecker : ts . TypeChecker ,
18
+ ) : ts . TransformerFactory < ts . SourceFile > {
18
19
return function collapseIntersectionInterfacesTransformFactory ( context : ts . TransformationContext ) {
19
20
return function collapseIntersectionInterfacesTransform ( sourceFile : ts . SourceFile ) {
20
21
const visited = ts . visitEachChild ( sourceFile , visitor , context ) ;
@@ -31,28 +32,121 @@ export function collapseIntersectionInterfacesTransformFactoryFactory(
31
32
}
32
33
33
34
function visitTypeAliasDeclaration ( node : ts . TypeAliasDeclaration ) {
34
- if (
35
- ts . isIntersectionTypeNode ( node . type )
36
- && node . type . types . every ( ts . isTypeLiteralNode )
37
- ) {
38
- // We need cast `node.type.types` to `ts.NodeArray<ts.TypeLiteralNode>`
39
- // because TypeScript can't figure out `node.type.types.every(ts.isTypeLiteralNode)`
40
- const allMembers = ( node . type . types as ts . NodeArray < ts . TypeLiteralNode > )
41
- . map ( ( type ) => type . members )
42
- . reduce ( ( all , members ) => ts . createNodeArray ( all . concat ( members ) ) , ts . createNodeArray ( [ ] ) ) ;
43
-
35
+ if ( ts . isIntersectionTypeNode ( node . type ) ) {
44
36
return ts . createTypeAliasDeclaration (
45
37
[ ] ,
46
38
[ ] ,
47
39
node . name . text ,
48
40
[ ] ,
49
- ts . createTypeLiteralNode ( allMembers ) ,
41
+ visitIntersectionTypeNode ( node . type ) ,
50
42
) ;
51
43
}
52
44
53
45
return node ;
54
46
}
55
- }
56
- }
57
- }
58
47
48
+ function visitIntersectionTypeNode ( node : ts . IntersectionTypeNode ) {
49
+ // Only intersection of type literals can be colapsed.
50
+ // We are currently ignoring intersections such as `{foo: string} & {bar: string} & TypeRef`
51
+ // TODO: handle mix of type references and multiple literal types
52
+ if ( ! node . types . every ( typeNode => ts . isTypeLiteralNode ( typeNode ) ) ) {
53
+ return node ;
54
+ }
55
+
56
+ // We need cast `node.type.types` to `ts.NodeArray<ts.TypeLiteralNode>`
57
+ // because TypeScript can't figure out `node.type.types.every(ts.isTypeLiteralNode)`
58
+ const types = node . types as ts . NodeArray < ts . TypeLiteralNode > ;
59
+
60
+ // Build a map of member names to all of types found in intersectioning type literals
61
+ // For instance {foo: string, bar: number} & { foo: number } will result in a map like this:
62
+ // Map {
63
+ // 'foo' => Set { 'string', 'number' },
64
+ // 'bar' => Set { 'number' }
65
+ // }
66
+ const membersMap = new Map < string | symbol , Set < ts . TypeNode > > ( ) ;
67
+
68
+ // A sepecial member of type literal nodes is index signitures which don't have a name
69
+ // We use this symbol to track it in our members map
70
+ const INDEX_SIGNITUTRE_MEMBER = Symbol ( 'Index signiture member' ) ;
71
+
72
+ // Keep a reference of first index signiture member parameters. (ignore rest)
73
+ let indexMemberParameter : ts . NodeArray < ts . ParameterDeclaration > | null = null ;
74
+
75
+ // Iterate through all of type literal nodes members and add them to the members map
76
+ types . forEach ( typeNode => {
77
+ typeNode . members . forEach ( member => {
78
+ if ( ts . isIndexSignatureDeclaration ( member ) ) {
79
+ if ( member . type !== undefined ) {
80
+ if ( membersMap . has ( INDEX_SIGNITUTRE_MEMBER ) ) {
81
+ membersMap . get ( INDEX_SIGNITUTRE_MEMBER ) ! . add ( member . type ) ;
82
+ } else {
83
+ indexMemberParameter = member . parameters ;
84
+ membersMap . set ( INDEX_SIGNITUTRE_MEMBER , new Set ( [ member . type ] ) ) ;
85
+ }
86
+ }
87
+ } else if ( ts . isPropertySignature ( member ) ) {
88
+ if ( member . type !== undefined ) {
89
+ let memberName = member . name . getText ( sourceFile ) ;
90
+
91
+ // For unknown reasons, member.name.getText() is returning nothing in some cases
92
+ // This is probably because previous transformers did something with the AST that
93
+ // index of text string of member identifier is lost
94
+ // TODO: investigate
95
+ if ( ! memberName ) {
96
+ memberName = ( member . name as any ) . escapedText ;
97
+ }
98
+
99
+ if ( membersMap . has ( memberName ) ) {
100
+ membersMap . get ( memberName ) ! . add ( member . type ) ;
101
+ } else {
102
+ membersMap . set ( memberName , new Set ( [ member . type ] ) ) ;
103
+ }
104
+ }
105
+ }
106
+ } ) ;
107
+ } ) ;
108
+
109
+ // Result type literal members list
110
+ const finalMembers : Array < ts . PropertySignature | ts . IndexSignatureDeclaration > = [ ] ;
111
+
112
+ // Put together the map into a type literal that has member per each map entery and type of that
113
+ // member is a union of all types in vlues for that member name in members map
114
+ // if a member has only one type, create a simple type literal for it
115
+ for ( const [ name , types ] of membersMap . entries ( ) ) {
116
+ if ( typeof name === 'symbol' ) {
117
+ continue ;
118
+ }
119
+ // if for this name there is only one type found use the first type, otherwise make a union of all types
120
+ let resultType = types . size === 1 ? Array . from ( types ) [ 0 ] : createUnionType ( Array . from ( types ) ) ;
121
+
122
+ finalMembers . push ( ts . createPropertySignature ( [ ] , name , undefined , resultType , undefined ) ) ;
123
+ }
124
+
125
+ // Handle index signiture member
126
+ if ( membersMap . has ( INDEX_SIGNITUTRE_MEMBER ) ) {
127
+ const indexTypes = Array . from ( membersMap . get ( INDEX_SIGNITUTRE_MEMBER ) ! ) ;
128
+ let indexType = indexTypes [ 0 ] ;
129
+ if ( indexTypes . length > 1 ) {
130
+ indexType = createUnionType ( indexTypes ) ;
131
+ }
132
+ const indexSigniture = ts . createIndexSignature ( [ ] , [ ] , indexMemberParameter ! , indexType ) ;
133
+ finalMembers . push ( indexSigniture ) ;
134
+ }
135
+
136
+ // Generate one single type literal node
137
+ return ts . createTypeLiteralNode ( finalMembers ) ;
138
+ }
139
+
140
+ /**
141
+ * Create a union type from multiple type nodes
142
+ * @param types
143
+ */
144
+ function createUnionType ( types : ts . TypeNode [ ] ) {
145
+ // first dedupe literal types
146
+ // TODO: this only works if all types are primitive types like string or number
147
+ const uniqueTypes = _ . uniqBy ( types , type => type . kind ) ;
148
+ return ts . createUnionOrIntersectionTypeNode ( ts . SyntaxKind . UnionType , uniqueTypes ) ;
149
+ }
150
+ } ;
151
+ } ;
152
+ }
0 commit comments