Skip to content

Commit 1e17c1b

Browse files
authored
Avoid repeated state type members (lyft#22)
1 parent 23cdac4 commit 1e17c1b

10 files changed

+173
-37
lines changed

.vscode/settings.json

+13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
{
2+
"files.exclude": {
3+
"**/.git": true,
4+
"**/.svn": true,
5+
"**/.hg": true,
6+
"**/CVS": true,
7+
"**/.DS_Store": true,
8+
"**/node_modules": true,
9+
"**/dist": true
10+
},
11+
"search.exclude": {
12+
"**/node_modules": true,
13+
"**/dist": true
14+
},
215
"typescript.tsdk": "node_modules/typescript/lib"
316
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as ts from 'typescript';
2+
import * as _ from 'lodash';
23

34
import * as helpers from '../helpers';
45

@@ -13,8 +14,8 @@ import * as helpers from '../helpers';
1314
* type Foo = {foo: string; bar: number;}
1415
*/
1516
export 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+
}

src/transforms/react-move-prop-types-to-class-transform.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
3333
export function reactMovePropTypesToClassTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
3434
return function reactMovePropTypesToClassTransformFactory(context: ts.TransformationContext) {
3535
return function reactMovePropTypesToClassTransform(sourceFile: ts.SourceFile) {
36-
return visitSourceFile(sourceFile, typeChecker);
36+
const visited = visitSourceFile(sourceFile, typeChecker);
37+
ts.addEmitHelpers(visited, context.readEmitHelpers());
38+
return visited;
3739
};
3840
};
3941
}

src/transforms/react-remove-prop-types-assignment-transform.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
1515
* After
1616
* class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> {}
1717
*/
18-
export function reactRemovePropTypesAssignmentTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory{
18+
export function reactRemovePropTypesAssignmentTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
1919
return function reactRemovePropTypesAssignmentTransformFactory(context: ts.TransformationContext) {
2020
return function reactRemovePropTypesAssignmentTransform(sourceFile: ts.SourceFile) {
21-
return ts.updateSourceFileNode(
21+
const visited = ts.updateSourceFileNode(
2222
sourceFile,
2323
sourceFile.statements.filter(s => !helpers.isReactPropTypeAssignmentStatement(s)),
2424
);
25-
}
26-
}
25+
ts.addEmitHelpers(visited, context.readEmitHelpers());
26+
return visited;
27+
};
28+
};
2729
}

src/transforms/react-remove-prop-types-import.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
2020
export function reactRemovePropTypesImportTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
2121
return function reactRemovePropTypesImportTransformFactory(context: ts.TransformationContext) {
2222
return function reactRemovePropTypesImportTransform(sourceFile: ts.SourceFile) {
23-
return ts.updateSourceFileNode(
23+
const visited = ts.updateSourceFileNode(
2424
sourceFile,
2525
sourceFile.statements
2626
.filter(s => {
@@ -32,6 +32,8 @@ export function reactRemovePropTypesImportTransformFactoryFactory(typeChecker: t
3232
})
3333
.map(updateReactImportIfNeeded),
3434
);
35+
ts.addEmitHelpers(visited, context.readEmitHelpers());
36+
return visited;
3537
};
3638
};
3739
}

src/transforms/react-remove-static-prop-types-member-transform.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
2121
export function reactRemoveStaticPropTypesMemberTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
2222
return function reactRemoveStaticPropTypesMemberTransformFactory(context: ts.TransformationContext) {
2323
return function reactRemoveStaticPropTypesMemberTransform(sourceFile: ts.SourceFile) {
24-
return ts.visitEachChild(sourceFile, visitor, context);
24+
const visited = ts.visitEachChild(sourceFile, visitor, context);
25+
ts.addEmitHelpers(visited, context.readEmitHelpers());
26+
return visited;
2527

2628
function visitor(node: ts.Node) {
2729
if (ts.isClassDeclaration(node) && helpers.isReactComponent(node, typeChecker)) {
@@ -32,29 +34,29 @@ export function reactRemoveStaticPropTypesMemberTransformFactoryFactory(typeChec
3234
node.name,
3335
node.typeParameters,
3436
ts.createNodeArray(node.heritageClauses),
35-
node.members.filter((member) => {
37+
node.members.filter(member => {
3638
if (
37-
ts.isPropertyDeclaration(member)
38-
&& helpers.hasStaticModifier(member)
39-
&& helpers.isPropTypesMember(member, sourceFile)
39+
ts.isPropertyDeclaration(member) &&
40+
helpers.hasStaticModifier(member) &&
41+
helpers.isPropTypesMember(member, sourceFile)
4042
) {
4143
return false;
4244
}
4345

4446
// propTypes getter
4547
if (
46-
ts.isGetAccessorDeclaration(member)
47-
&& helpers.hasStaticModifier(member)
48-
&& helpers.isPropTypesMember(member, sourceFile)
48+
ts.isGetAccessorDeclaration(member) &&
49+
helpers.hasStaticModifier(member) &&
50+
helpers.isPropTypesMember(member, sourceFile)
4951
) {
5052
return false;
5153
}
5254
return true;
5355
}),
54-
)
56+
);
5557
}
5658
return node;
5759
}
58-
}
59-
}
60+
};
61+
};
6062
}

src/transforms/react-stateless-function-make-props-transform.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
3636
export function reactStatelessFunctionMakePropsTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
3737
return function reactStatelessFunctionMakePropsTransformFactory(context: ts.TransformationContext) {
3838
return function reactStatelessFunctionMakePropsTransform(sourceFile: ts.SourceFile) {
39-
return visitSourceFile(sourceFile, typeChecker);
39+
const visited = visitSourceFile(sourceFile, typeChecker);
40+
ts.addEmitHelpers(visited, context.readEmitHelpers());
41+
return visited;
4042
};
4143
};
4244
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type A = { foo: string; } & { foo: string; };
2+
3+
type B = { foo: string; bar: number; } & { foo: number; bar: number; }
4+
5+
type C = { foo: string; bar: number; } & { foo: number; bar: number; } & { foo: string; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
type A = {
2+
foo: string,
3+
};
4+
5+
type B = {
6+
foo: string | number,
7+
bar: number,
8+
};
9+
10+
type C = {
11+
foo: string | number,
12+
bar: number,
13+
};

tsconfig.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
"module": "commonjs",
88
"emitDecoratorMetadata": true,
99
"experimentalDecorators": true,
10+
"downlevelIteration": true,
1011
"sourceMap": true,
1112
"outDir": "dist",
12-
"sourceRoot": "../src"
13+
"sourceRoot": "../src",
14+
"lib": ["dom", "es2015"]
1315
},
1416
"exclude": ["node_modules", "test", "dist"],
15-
"types": ["node", "jest"],
16-
"lib": ["es2017"]
17+
"types": ["node", "jest"]
1718
}

0 commit comments

Comments
 (0)