|
| 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