/
IntersectionNodeParser.ts
105 lines (91 loc) · 3.83 KB
/
IntersectionNodeParser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import ts from "typescript";
import { Context, NodeParser } from "../NodeParser.js";
import { SubNodeParser } from "../SubNodeParser.js";
import { BaseType } from "../Type/BaseType.js";
import { IntersectionType } from "../Type/IntersectionType.js";
import { PrimitiveType } from "../Type/PrimitiveType.js";
import { UnionType } from "../Type/UnionType.js";
import { derefType } from "../Utils/derefType.js";
import { uniqueTypeArray } from "../Utils/uniqueTypeArray.js";
import { UndefinedType } from "../Type/UndefinedType.js";
import { NeverType } from "../Type/NeverType.js";
import { ObjectType } from "../Type/ObjectType.js";
import { StringType } from "../Type/StringType.js";
export class IntersectionNodeParser implements SubNodeParser {
public constructor(
protected typeChecker: ts.TypeChecker,
protected childNodeParser: NodeParser,
) {}
public supportsNode(node: ts.IntersectionTypeNode): boolean {
return node.kind === ts.SyntaxKind.IntersectionType;
}
public createType(node: ts.IntersectionTypeNode, context: Context): BaseType {
const types = node.types.map((subnode) => this.childNodeParser.createType(subnode, context));
// if any type is never, the intersection type resolves to never
if (types.filter((t) => t instanceof NeverType).length) {
return new NeverType();
}
// handle autocomplete hacks like `string & {}`
if (types.length === 2 && types.some((t) => t instanceof StringType) && types.some((t) => isEmptyObject(t))) {
return new StringType(true);
}
return translate(types);
}
}
function isEmptyObject(x: BaseType) {
const t = derefType(x);
return t instanceof ObjectType && !t.getAdditionalProperties() && !t.getProperties().length;
}
function derefAndFlattenUnions(type: BaseType): BaseType[] {
const derefed = derefType(type);
return derefed instanceof UnionType
? derefed.getTypes().reduce((result: BaseType[], derefedType: BaseType) => {
result.push(...derefAndFlattenUnions(derefedType));
return result;
}, [])
: [type];
}
/**
* Translates the given intersection type into a union type if necessary so `A & (B | C)` becomes
* `(A & B) | (A & C)`. If no translation is needed then the original intersection type is returned.
*/
export function translate(types: BaseType[]): BaseType {
types = uniqueTypeArray(types);
if (types.length == 1) {
return types[0];
}
const unions = types.map(derefAndFlattenUnions);
const result: BaseType[] = [];
function process(i: number, t: BaseType[] = []) {
for (const type of unions[i]) {
let currentTypes = [...t, type];
if (i < unions.length - 1) {
process(i + 1, currentTypes);
} else {
currentTypes = uniqueTypeArray(currentTypes);
if (currentTypes.some((c) => c instanceof UndefinedType)) {
// never
result.push(new UndefinedType());
} else {
const primitives = currentTypes.filter((c) => c instanceof PrimitiveType);
if (primitives.length === 1) {
result.push(primitives[0]);
} else if (primitives.length > 1) {
// conflict -> ignore
} else if (currentTypes.length === 1) {
result.push(currentTypes[0]);
} else {
result.push(new IntersectionType(currentTypes));
}
}
}
}
}
process(0);
if (result.length === 1) {
return result[0];
} else if (result.length > 1) {
return new UnionType(result);
}
throw new Error("Could not translate intersection to union.");
}