Skip to content

Commit e92a2b3

Browse files
committed
chore(transformer): Refactor branch logic and support conditional typing
1 parent 4959ed9 commit e92a2b3

File tree

2 files changed

+75
-153
lines changed

2 files changed

+75
-153
lines changed
Lines changed: 34 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
import ts from 'typescript';
2-
import { GetTsAutoMockOverloadOptions, TsAutoMockOverloadOptions } from '../../../options/overload';
3-
import { TypescriptCreator } from '../../helper/creator';
4-
import { MockDefiner } from '../../mockDefiner/mockDefiner';
5-
import { ModuleName } from '../../mockDefiner/modules/moduleName';
6-
import { MockIdentifierGenericParameterValue } from '../../mockIdentifier/mockIdentifier';
2+
import { MethodSignature, TypescriptCreator } from '../../helper/creator';
73
import { Scope } from '../../scope/scope';
4+
import { ResolveSignatureElseBranch } from '../helper/branching';
85
import { TypescriptHelper } from '../helper/helper';
96
import { GetNullDescriptor } from '../null/null';
10-
import { GetDescriptor } from '../descriptor';
117
import { GetTypeParameterDescriptor } from '../typeParameter/typeParameter';
128

13-
export interface MethodSignature {
14-
parameters?: ts.TypeNode[];
15-
returnValue: ts.Expression;
16-
}
17-
189
function isDeclarationWithTypeParameterChildren(node: ts.Node): node is ts.DeclarationWithTypeParameterChildren {
1910
return ts.isFunctionLike(node) ||
2011
ts.isClassLike(node) ||
@@ -48,12 +39,16 @@ export function GetConditionalTypeDescriptor(node: ts.ConditionalTypeNode, scope
4839
return GetNullDescriptor();
4940
}
5041

42+
const statements: ts.Statement[] = [];
43+
5144
const genericValue: ts.CallExpression = GetTypeParameterDescriptor(declaration, scope);
5245

53-
const statements: ts.Statement[] = [];
46+
const signatures: MethodSignature[] = ConstructSignatures(node);
47+
const [signature]: MethodSignature[] = signatures;
5448

49+
const parameterIdentifier: ts.Identifier = TypescriptHelper.ExtractFirstIdentifier(signature.parameters[0].name);
5550
const valueDeclaration: ts.VariableDeclaration = TypescriptCreator.createVariableDeclaration(
56-
MockIdentifierGenericParameterValue,
51+
parameterIdentifier,
5752
genericValue,
5853
);
5954

@@ -63,157 +58,43 @@ export function GetConditionalTypeDescriptor(node: ts.ConditionalTypeNode, scope
6358
]),
6459
);
6560

66-
statements.push(ResolveSignatureElseBranch(new Map(), ConstructSignatures(node, scope), [valueDeclaration]));
61+
const typeVariableMap: Map<ts.TypeNode, ts.StringLiteral | ts.Identifier> = new Map(
62+
signatures.reduce((typeHashTuples: [ts.TypeNode, ts.StringLiteral][], s: MethodSignature) => {
63+
const [parameter]: typeof s.parameters | [undefined] = s.parameters;
64+
if (!parameter) {
65+
return typeHashTuples;
66+
}
67+
68+
if (ts.isFunctionLike(parameter.type)) {
69+
typeHashTuples.push([
70+
parameter.type,
71+
ts.createStringLiteral(
72+
TypescriptCreator.createSignatureHash(parameter.type),
73+
),
74+
]);
75+
}
76+
77+
return typeHashTuples;
78+
}, [] as [ts.TypeNode, ts.StringLiteral][]),
79+
);
80+
81+
statements.push(ResolveSignatureElseBranch(typeVariableMap, signatures, signature, scope));
6782

6883
return TypescriptCreator.createIIFE(ts.createBlock(statements, true));
6984
}
7085

71-
function ConstructSignatures(node: ts.ConditionalTypeNode, scope: Scope, signatures: MethodSignature[] = []): MethodSignature[] {
72-
const parameters: ts.TypeNode[] = [node.extendsType];
73-
86+
function ConstructSignatures(node: ts.ConditionalTypeNode, signatures: MethodSignature[] = []): MethodSignature[] {
7487
if (ts.isConditionalTypeNode(node.trueType)) {
75-
return ConstructSignatures(node.trueType, scope, signatures);
88+
return ConstructSignatures(node.trueType, signatures);
7689
}
7790

78-
signatures.push({
79-
parameters,
80-
returnValue: GetDescriptor(node.trueType, scope),
81-
});
91+
signatures.push(TypescriptCreator.createMethodSignature([node.extendsType], node.trueType));
8292

8393
if (ts.isConditionalTypeNode(node.falseType)) {
84-
return ConstructSignatures(node.falseType, scope, signatures);
94+
return ConstructSignatures(node.falseType, signatures);
8595
}
8696

87-
signatures.push({
88-
parameters,
89-
returnValue: GetDescriptor(node.falseType, scope),
90-
});
97+
signatures.push(TypescriptCreator.createMethodSignature(undefined, node.falseType));
9198

9299
return signatures;
93100
}
94-
95-
function CreateTypeEquality(signatureType: ts.Identifier | ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration | ts.VariableDeclaration): ts.Expression {
96-
// TODO: Factor this into a helper - guess it can be helpful in other places.
97-
let declarationName: ts.BindingName = primaryDeclaration.name;
98-
99-
while (!ts.isIdentifier(declarationName)) {
100-
const [bindingElement]: Array<ts.BindingElement | undefined> = (declarationName.elements as ts.NodeArray<ts.ArrayBindingElement>).filter(ts.isBindingElement);
101-
if (!bindingElement) {
102-
throw new Error('Failed to find an identifier for the primary declaration!');
103-
}
104-
105-
declarationName = bindingElement.name;
106-
}
107-
108-
if (!signatureType) {
109-
return ts.createPrefix(
110-
ts.SyntaxKind.ExclamationToken,
111-
ts.createPrefix(
112-
ts.SyntaxKind.ExclamationToken,
113-
declarationName,
114-
),
115-
);
116-
}
117-
118-
if (TypescriptHelper.IsLiteralOrPrimitive(signatureType)) {
119-
return ts.createStrictEquality(
120-
ts.createTypeOf(declarationName),
121-
signatureType ? ts.createStringLiteral(signatureType.getText()) : ts.createVoidZero(),
122-
);
123-
}
124-
125-
if (ts.isIdentifier(signatureType)) {
126-
return ts.createStrictEquality(
127-
ts.createPropertyAccess(declarationName, '__factory'),
128-
signatureType,
129-
);
130-
}
131-
132-
return ts.createBinary(declarationName, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier('Object'));
133-
}
134-
135-
function CreateUnionTypeOfEquality(signatureType: ts.Identifier | ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration | ts.VariableDeclaration): ts.Expression {
136-
const typeNodesAndVariableReferences: Array<ts.TypeNode | ts.Identifier> = [];
137-
138-
if (signatureType) {
139-
if (ts.isTypeNode(signatureType) && ts.isUnionTypeNode(signatureType)) {
140-
typeNodesAndVariableReferences.push(...signatureType.types);
141-
} else {
142-
typeNodesAndVariableReferences.push(signatureType);
143-
}
144-
}
145-
146-
const [firstType, ...remainingTypes]: Array<ts.TypeNode | ts.Identifier> = typeNodesAndVariableReferences;
147-
148-
return remainingTypes.reduce(
149-
(prevStatement: ts.Expression, typeNode: ts.TypeNode) =>
150-
ts.createLogicalOr(
151-
prevStatement,
152-
CreateTypeEquality(typeNode, primaryDeclaration),
153-
),
154-
CreateTypeEquality(firstType, primaryDeclaration),
155-
);
156-
}
157-
158-
function ResolveParameterBranch(
159-
declarationVariableMap: Map<ts.Declaration, ts.Identifier>,
160-
declarations: ts.TypeNode[],
161-
allDeclarations: Array<ts.ParameterDeclaration | ts.VariableDeclaration>,
162-
returnValue: ts.Expression,
163-
elseBranch: ts.Statement,
164-
): ts.Statement {
165-
const [firstDeclaration, ...remainingDeclarations]: Array<ts.TypeNode | undefined> = declarations;
166-
167-
const variableReferenceOrType: (t: ts.TypeNode | undefined) => ts.Identifier | ts.TypeNode | undefined = (t: ts.TypeNode | undefined) => t;
168-
// const variableReferenceOrType: (declaration: ts.ParameterDeclaration) => ts.Identifier | ts.TypeNode | undefined =
169-
// (declaration: ts.ParameterDeclaration) => {
170-
// if (declarationVariableMap.has(declaration)) {
171-
// return declarationVariableMap.get(declaration);
172-
// } else {
173-
// return declaration.type;
174-
// }
175-
// };
176-
177-
// TODO: These conditions quickly grow in size, but it should be possible to
178-
// squeeze things together and optimize it with something like:
179-
//
180-
// const typeOf = function (left, right) { return typeof left === right; }
181-
// const evaluate = (function(left, right) { return this._ = this._ || typeOf(left, right); }).bind({})
182-
//
183-
// if (evaluate(firstArg, 'boolean') && evaluate(secondArg, 'number') && ...) {
184-
// ...
185-
// }
186-
//
187-
// `this._' acts as a cache, since the control flow may evaluate the same
188-
// conditions multiple times.
189-
const condition: ts.Expression = remainingDeclarations.reduce(
190-
(prevStatement: ts.Expression, node: ts.TypeNode | undefined, index: number) =>
191-
ts.createLogicalAnd(
192-
prevStatement,
193-
CreateUnionTypeOfEquality(variableReferenceOrType(node), allDeclarations[index + 1]),
194-
),
195-
CreateUnionTypeOfEquality(variableReferenceOrType(firstDeclaration), allDeclarations[0]),
196-
);
197-
198-
return ts.createIf(condition, ts.createReturn(returnValue), elseBranch);
199-
}
200-
201-
export function ResolveSignatureElseBranch(
202-
declarationVariableMap: Map<ts.ParameterDeclaration, ts.Identifier>,
203-
signatures: MethodSignature[],
204-
longestParameterList: Array<ts.ParameterDeclaration | ts.VariableDeclaration>,
205-
): ts.Statement {
206-
const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions();
207-
208-
const [signature, ...remainingSignatures]: MethodSignature[] = signatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst);
209-
210-
const indistinctSignatures: boolean = signatures.every((sig: MethodSignature) => !sig.parameters?.length);
211-
if (!remainingSignatures.length || indistinctSignatures) {
212-
return ts.createReturn(signature.returnValue);
213-
}
214-
215-
const elseBranch: ts.Statement = ResolveSignatureElseBranch(declarationVariableMap, remainingSignatures, longestParameterList);
216-
217-
const currentParameters: ts.TypeNode[] = signature.parameters || [];
218-
return ResolveParameterBranch(declarationVariableMap, currentParameters, longestParameterList, signature.returnValue, elseBranch);
219-
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createMock } from 'ts-auto-mock';
2+
3+
describe('conditional typing', () => {
4+
it('should branch properly based on passed generic parameter', () => {
5+
6+
type TypeName<T> =
7+
T extends string ? 'string' :
8+
T extends number ? 'number' :
9+
T extends boolean ? 'boolean' :
10+
T extends undefined ? 'undefined' :
11+
T extends Function ? 'function' :
12+
T extends () => number ? 'function' :
13+
T extends (a: number) => number ? 'function' :
14+
'object';
15+
16+
interface Test {
17+
a: TypeName<string>;
18+
b: TypeName<number>;
19+
c: TypeName<boolean>;
20+
d: TypeName<undefined>;
21+
e: TypeName<Function>;
22+
f: TypeName<() => number>;
23+
g: TypeName<(a: number) => number>;
24+
// TODO: This one (unknown) is not possible for now. It's mocked as
25+
// undefined and will result in the same outcome as `d`.
26+
// h: TypeName<unknown>;
27+
i: TypeName<[]>;
28+
}
29+
30+
const mock: Test = createMock<Test>();
31+
expect(mock.a).toBe('string');
32+
expect(mock.b).toBe('number');
33+
expect(mock.c).toBe('boolean');
34+
expect(mock.d).toBe('undefined');
35+
expect(mock.e).toBe('function');
36+
expect(mock.f).toBe('function');
37+
expect(mock.g).toBe('function');
38+
// expect(mock.h).toBe('object');
39+
expect(mock.i).toBe('object');
40+
});
41+
});

0 commit comments

Comments
 (0)