Skip to content
This repository was archived by the owner on Sep 21, 2019. It is now read-only.

Avoid repeated state type members #22

Merged
merged 10 commits into from
Feb 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
{
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/dist": true
},
"search.exclude": {
"**/node_modules": true,
"**/dist": true
},
"typescript.tsdk": "node_modules/typescript/lib"
}
126 changes: 110 additions & 16 deletions src/transforms/collapse-intersection-interfaces-transform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as ts from 'typescript';
import * as _ from 'lodash';

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

Expand All @@ -13,8 +14,8 @@ import * as helpers from '../helpers';
* type Foo = {foo: string; bar: number;}
*/
export function collapseIntersectionInterfacesTransformFactoryFactory(
typeChecker: ts.TypeChecker,
): ts.TransformerFactory<ts.SourceFile> {
typeChecker: ts.TypeChecker,
): ts.TransformerFactory<ts.SourceFile> {
return function collapseIntersectionInterfacesTransformFactory(context: ts.TransformationContext) {
return function collapseIntersectionInterfacesTransform(sourceFile: ts.SourceFile) {
const visited = ts.visitEachChild(sourceFile, visitor, context);
Expand All @@ -31,28 +32,121 @@ export function collapseIntersectionInterfacesTransformFactoryFactory(
}

function visitTypeAliasDeclaration(node: ts.TypeAliasDeclaration) {
if (
ts.isIntersectionTypeNode(node.type)
&& node.type.types.every(ts.isTypeLiteralNode)
) {
// We need cast `node.type.types` to `ts.NodeArray<ts.TypeLiteralNode>`
// because TypeScript can't figure out `node.type.types.every(ts.isTypeLiteralNode)`
const allMembers = (node.type.types as ts.NodeArray<ts.TypeLiteralNode>)
.map((type) => type.members)
.reduce((all, members) => ts.createNodeArray(all.concat(members)), ts.createNodeArray([]));

if (ts.isIntersectionTypeNode(node.type)) {
return ts.createTypeAliasDeclaration(
[],
[],
node.name.text,
[],
ts.createTypeLiteralNode(allMembers),
visitIntersectionTypeNode(node.type),
);
}

return node;
}
}
}
}

function visitIntersectionTypeNode(node: ts.IntersectionTypeNode) {
// Only intersection of type literals can be colapsed.
// We are currently ignoring intersections such as `{foo: string} & {bar: string} & TypeRef`
// TODO: handle mix of type references and multiple literal types
if (!node.types.every(typeNode => ts.isTypeLiteralNode(typeNode))) {
return node;
}

// We need cast `node.type.types` to `ts.NodeArray<ts.TypeLiteralNode>`
// because TypeScript can't figure out `node.type.types.every(ts.isTypeLiteralNode)`
const types = node.types as ts.NodeArray<ts.TypeLiteralNode>;

// Build a map of member names to all of types found in intersectioning type literals
// For instance {foo: string, bar: number} & { foo: number } will result in a map like this:
// Map {
// 'foo' => Set { 'string', 'number' },
// 'bar' => Set { 'number' }
// }
const membersMap = new Map<string | symbol, Set<ts.TypeNode>>();

// A sepecial member of type literal nodes is index signitures which don't have a name
// We use this symbol to track it in our members map
const INDEX_SIGNITUTRE_MEMBER = Symbol('Index signiture member');

// Keep a reference of first index signiture member parameters. (ignore rest)
let indexMemberParameter: ts.NodeArray<ts.ParameterDeclaration> | null = null;

// Iterate through all of type literal nodes members and add them to the members map
types.forEach(typeNode => {
typeNode.members.forEach(member => {
if (ts.isIndexSignatureDeclaration(member)) {
if (member.type !== undefined) {
if (membersMap.has(INDEX_SIGNITUTRE_MEMBER)) {
membersMap.get(INDEX_SIGNITUTRE_MEMBER)!.add(member.type);
} else {
indexMemberParameter = member.parameters;
membersMap.set(INDEX_SIGNITUTRE_MEMBER, new Set([member.type]));
}
}
} else if (ts.isPropertySignature(member)) {
if (member.type !== undefined) {
let memberName = member.name.getText(sourceFile);

// For unknown reasons, member.name.getText() is returning nothing in some cases
// This is probably because previous transformers did something with the AST that
// index of text string of member identifier is lost
// TODO: investigate
if (!memberName) {
memberName = (member.name as any).escapedText;
}

if (membersMap.has(memberName)) {
membersMap.get(memberName)!.add(member.type);
} else {
membersMap.set(memberName, new Set([member.type]));
}
}
}
});
});

// Result type literal members list
const finalMembers: Array<ts.PropertySignature | ts.IndexSignatureDeclaration> = [];

// Put together the map into a type literal that has member per each map entery and type of that
// member is a union of all types in vlues for that member name in members map
// if a member has only one type, create a simple type literal for it
for (const [name, types] of membersMap.entries()) {
if (typeof name === 'symbol') {
continue;
}
// if for this name there is only one type found use the first type, otherwise make a union of all types
let resultType = types.size === 1 ? Array.from(types)[0] : createUnionType(Array.from(types));

finalMembers.push(ts.createPropertySignature([], name, undefined, resultType, undefined));
}

// Handle index signiture member
if (membersMap.has(INDEX_SIGNITUTRE_MEMBER)) {
const indexTypes = Array.from(membersMap.get(INDEX_SIGNITUTRE_MEMBER)!);
let indexType = indexTypes[0];
if (indexTypes.length > 1) {
indexType = createUnionType(indexTypes);
}
const indexSigniture = ts.createIndexSignature([], [], indexMemberParameter!, indexType);
finalMembers.push(indexSigniture);
}

// Generate one single type literal node
return ts.createTypeLiteralNode(finalMembers);
}

/**
* Create a union type from multiple type nodes
* @param types
*/
function createUnionType(types: ts.TypeNode[]) {
// first dedupe literal types
// TODO: this only works if all types are primitive types like string or number
const uniqueTypes = _.uniqBy(types, type => type.kind);
return ts.createUnionOrIntersectionTypeNode(ts.SyntaxKind.UnionType, uniqueTypes);
}
};
};
}
4 changes: 3 additions & 1 deletion src/transforms/react-move-prop-types-to-class-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
export function reactMovePropTypesToClassTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
return function reactMovePropTypesToClassTransformFactory(context: ts.TransformationContext) {
return function reactMovePropTypesToClassTransform(sourceFile: ts.SourceFile) {
return visitSourceFile(sourceFile, typeChecker);
const visited = visitSourceFile(sourceFile, typeChecker);
ts.addEmitHelpers(visited, context.readEmitHelpers());
return visited;
};
};
}
Expand Down
10 changes: 6 additions & 4 deletions src/transforms/react-remove-prop-types-assignment-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
* After
* class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> {}
*/
export function reactRemovePropTypesAssignmentTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory{
export function reactRemovePropTypesAssignmentTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
return function reactRemovePropTypesAssignmentTransformFactory(context: ts.TransformationContext) {
return function reactRemovePropTypesAssignmentTransform(sourceFile: ts.SourceFile) {
return ts.updateSourceFileNode(
const visited = ts.updateSourceFileNode(
sourceFile,
sourceFile.statements.filter(s => !helpers.isReactPropTypeAssignmentStatement(s)),
);
}
}
ts.addEmitHelpers(visited, context.readEmitHelpers());
return visited;
};
};
}
4 changes: 3 additions & 1 deletion src/transforms/react-remove-prop-types-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
export function reactRemovePropTypesImportTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
return function reactRemovePropTypesImportTransformFactory(context: ts.TransformationContext) {
return function reactRemovePropTypesImportTransform(sourceFile: ts.SourceFile) {
return ts.updateSourceFileNode(
const visited = ts.updateSourceFileNode(
sourceFile,
sourceFile.statements
.filter(s => {
Expand All @@ -32,6 +32,8 @@ export function reactRemovePropTypesImportTransformFactoryFactory(typeChecker: t
})
.map(updateReactImportIfNeeded),
);
ts.addEmitHelpers(visited, context.readEmitHelpers());
return visited;
};
};
}
Expand Down
24 changes: 13 additions & 11 deletions src/transforms/react-remove-static-prop-types-member-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
export function reactRemoveStaticPropTypesMemberTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
return function reactRemoveStaticPropTypesMemberTransformFactory(context: ts.TransformationContext) {
return function reactRemoveStaticPropTypesMemberTransform(sourceFile: ts.SourceFile) {
return ts.visitEachChild(sourceFile, visitor, context);
const visited = ts.visitEachChild(sourceFile, visitor, context);
ts.addEmitHelpers(visited, context.readEmitHelpers());
return visited;

function visitor(node: ts.Node) {
if (ts.isClassDeclaration(node) && helpers.isReactComponent(node, typeChecker)) {
Expand All @@ -32,29 +34,29 @@ export function reactRemoveStaticPropTypesMemberTransformFactoryFactory(typeChec
node.name,
node.typeParameters,
ts.createNodeArray(node.heritageClauses),
node.members.filter((member) => {
node.members.filter(member => {
if (
ts.isPropertyDeclaration(member)
&& helpers.hasStaticModifier(member)
&& helpers.isPropTypesMember(member, sourceFile)
ts.isPropertyDeclaration(member) &&
helpers.hasStaticModifier(member) &&
helpers.isPropTypesMember(member, sourceFile)
) {
return false;
}

// propTypes getter
if (
ts.isGetAccessorDeclaration(member)
&& helpers.hasStaticModifier(member)
&& helpers.isPropTypesMember(member, sourceFile)
ts.isGetAccessorDeclaration(member) &&
helpers.hasStaticModifier(member) &&
helpers.isPropTypesMember(member, sourceFile)
) {
return false;
}
return true;
}),
)
);
}
return node;
}
}
}
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export type Factory = ts.TransformerFactory<ts.SourceFile>;
export function reactStatelessFunctionMakePropsTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory {
return function reactStatelessFunctionMakePropsTransformFactory(context: ts.TransformationContext) {
return function reactStatelessFunctionMakePropsTransform(sourceFile: ts.SourceFile) {
return visitSourceFile(sourceFile, typeChecker);
const visited = visitSourceFile(sourceFile, typeChecker);
ts.addEmitHelpers(visited, context.readEmitHelpers());
return visited;
};
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type A = { foo: string; } & { foo: string; };

type B = { foo: string; bar: number; } & { foo: number; bar: number; }

type C = { foo: string; bar: number; } & { foo: number; bar: number; } & { foo: string; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type A = {
foo: string,
};

type B = {
foo: string | number,
bar: number,
};

type C = {
foo: string | number,
bar: number,
};
7 changes: 4 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
"module": "commonjs",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"downlevelIteration": true,
"sourceMap": true,
"outDir": "dist",
"sourceRoot": "../src"
"sourceRoot": "../src",
"lib": ["dom", "es2015"]
},
"exclude": ["node_modules", "test", "dist"],
"types": ["node", "jest"],
"lib": ["es2017"]
"types": ["node", "jest"]
}