Skip to content

Commit d746c5b

Browse files
committed
consolidate type traversal for union/intersection types
1 parent dc0a323 commit d746c5b

File tree

1 file changed

+121
-0
lines changed

1 file changed

+121
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import ts from "typescript";
2+
3+
export interface TypeTraversalOptions<T> {
4+
/** TypeScript type checker */
5+
readonly checker: ts.TypeChecker;
6+
7+
/** Function to check each type and potentially return a result */
8+
readonly checkType: (type: ts.Type) => T | undefined;
9+
10+
/** How to combine results from union types (default: return first match) */
11+
readonly handleUnion?: "first" | "some" | "every";
12+
13+
/** How to combine results from intersection types (default: return first match) */
14+
readonly handleIntersection?: "first" | "some" | "every";
15+
16+
/** Whether to check apparent type (default: false) */
17+
readonly visitApparentType?: boolean;
18+
19+
/** Whether to traverse base types for interfaces (default: false) */
20+
readonly visitBaseTypes?: boolean;
21+
22+
/** Whether to traverse type reference targets (default: false) */
23+
readonly visitTypeReferenceTarget?: boolean;
24+
}
25+
26+
/**
27+
* Traverses a TypeScript type hierarchy with configurable behavior.
28+
* Handles unions, intersections, type references, and base types.
29+
* Uses a seen set to avoid infinite recursion on circular types.
30+
*
31+
* @param type - The type to traverse
32+
* @param options - Configuration for traversal behavior
33+
* @param seen - Set of already-visited types (for cycle detection)
34+
* @returns Result from checkType function, or undefined if no match found
35+
*/
36+
export function traverseTypeHierarchy<T>(
37+
type: ts.Type,
38+
options: TypeTraversalOptions<T>,
39+
seen = new Set<ts.Type>(),
40+
): T | undefined {
41+
// Prevent infinite recursion on circular types
42+
if (seen.has(type)) return undefined;
43+
seen.add(type);
44+
45+
// Check the type directly first
46+
const direct = options.checkType(type);
47+
if (direct !== undefined) return direct;
48+
49+
// Check apparent type if requested
50+
if (options.visitApparentType) {
51+
const apparent = options.checker.getApparentType(type);
52+
if (apparent !== type) {
53+
const fromApparent = traverseTypeHierarchy(apparent, options, seen);
54+
if (fromApparent !== undefined) return fromApparent;
55+
}
56+
}
57+
58+
// Handle union and intersection types
59+
if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) {
60+
const compound = type as ts.UnionOrIntersectionType;
61+
const isUnion = !!(type.flags & ts.TypeFlags.Union);
62+
const strategy = isUnion
63+
? (options.handleUnion ?? "first")
64+
: (options.handleIntersection ?? "first");
65+
66+
if (strategy === "first") {
67+
// Return first match
68+
for (const child of compound.types) {
69+
const result = traverseTypeHierarchy(child, options, seen);
70+
if (result !== undefined) return result;
71+
}
72+
// No match found in union/intersection
73+
return undefined;
74+
} else if (strategy === "some") {
75+
// Check if any child matches (for boolean results)
76+
const hasMatch = compound.types.some((child) =>
77+
traverseTypeHierarchy(child, options, seen)
78+
);
79+
return (hasMatch as unknown as T);
80+
} else if (strategy === "every") {
81+
// Check if all children match (for boolean results)
82+
const allMatch = compound.types.every((child) =>
83+
traverseTypeHierarchy(child, options, seen)
84+
);
85+
return (allMatch as unknown as T);
86+
}
87+
}
88+
89+
// Handle object types
90+
if (type.flags & ts.TypeFlags.Object) {
91+
const objectType = type as ts.ObjectType;
92+
93+
// Check type reference targets if requested
94+
if (
95+
options.visitTypeReferenceTarget &&
96+
objectType.objectFlags & ts.ObjectFlags.Reference
97+
) {
98+
const typeRef = objectType as ts.TypeReference;
99+
if (typeRef.target) {
100+
const fromTarget = traverseTypeHierarchy(typeRef.target, options, seen);
101+
if (fromTarget !== undefined) return fromTarget;
102+
}
103+
}
104+
105+
// Check base types if requested
106+
if (
107+
options.visitBaseTypes &&
108+
objectType.objectFlags & ts.ObjectFlags.ClassOrInterface
109+
) {
110+
const baseTypes = options.checker.getBaseTypes(
111+
objectType as ts.InterfaceType,
112+
) ?? [];
113+
for (const base of baseTypes) {
114+
const fromBase = traverseTypeHierarchy(base, options, seen);
115+
if (fromBase !== undefined) return fromBase;
116+
}
117+
}
118+
}
119+
120+
return undefined;
121+
}

0 commit comments

Comments
 (0)