11import * as ts from 'typescript' ;
2+ import * as _ from 'lodash' ;
23
34import * as helpers from '../helpers' ;
45
@@ -13,8 +14,8 @@ import * as helpers from '../helpers';
1314 * type Foo = {foo: string; bar: number;}
1415 */
1516export function collapseIntersectionInterfacesTransformFactoryFactory (
16- typeChecker : ts . TypeChecker ,
17- ) : ts . TransformerFactory < ts . SourceFile > {
17+ typeChecker : ts . TypeChecker ,
18+ ) : ts . TransformerFactory < ts . SourceFile > {
1819 return function collapseIntersectionInterfacesTransformFactory ( context : ts . TransformationContext ) {
1920 return function collapseIntersectionInterfacesTransform ( sourceFile : ts . SourceFile ) {
2021 const visited = ts . visitEachChild ( sourceFile , visitor , context ) ;
@@ -31,28 +32,121 @@ export function collapseIntersectionInterfacesTransformFactoryFactory(
3132 }
3233
3334 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 ) ) {
4436 return ts . createTypeAliasDeclaration (
4537 [ ] ,
4638 [ ] ,
4739 node . name . text ,
4840 [ ] ,
49- ts . createTypeLiteralNode ( allMembers ) ,
41+ visitIntersectionTypeNode ( node . type ) ,
5042 ) ;
5143 }
5244
5345 return node ;
5446 }
55- }
56- }
57- }
5847
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