import { hasKey, isRecord, memoizeFunction } from 'ts-data-forge'; import { hasRecordInternals, type Type } from '../type.mjs'; import { createAssertFn, createCastFn, createIsFn } from '../utils/index.mjs'; /** * Creates a recursive type definition for self-referential or mutually recursive data structures. * * This function enables defining types that refer to themselves (like linked lists, trees) * or mutually refer to each other (like even/odd number chains). * * @template A - The type to be defined recursively * @param typeName - A descriptive name for the recursive type * @param definition - A function that returns the Type definition. This function is called * lazily to allow self-reference * @param options - Optional configuration * @param options.defaultValue - Explicit default value for the type. Recommended for * mutually recursive types to avoid infinite loops * * @returns A Type that can validate and work with recursive structures * * @example * // Simple recursive type: Linked list * ```ts * import * as t from 'ts-fortress'; * * type LinkedList = Readonly<{ * value: number; * next: LinkedList | null; * }>; * * const LinkedListNumber: t.Type = t.recursion('LinkedList', () => * t.record({ * value: t.number(), * next: t.union([t.nullType, LinkedListNumber]), * }), * ); * ``` * * @example * // Tree structure * ```ts * import * as t from 'ts-fortress'; * * type TreeNode = Readonly<{ * value: T; * children: readonly TreeNode[]; * }>; * * const TreeNodeString: t.Type> = t.recursion( * 'TreeNode', * () => * t.record({ * value: t.string(), * children: t.array(TreeNodeString), * }), * ); * ``` * * @example * // Mutually recursive types: Even/Odd chain * ```ts * import * as t from 'ts-fortress'; * * type EvenNumber = Readonly<{ type: 'even'; next: OddNumber | null }>; * * type OddNumber = Readonly<{ type: 'odd'; next: EvenNumber | null }>; * * // IMPORTANT: For mutual recursion, place terminal types (like nullType) * // first in unions, or provide explicit defaultValue * const EvenNumber: t.Type = t.recursion('EvenNumber', () => * t.record({ * type: t.literal('even'), * next: t.union([t.nullType, OddNumber]), // nullType first! * }), * ); * * const OddNumber: t.Type = t.recursion('OddNumber', () => * t.record({ * type: t.literal('odd'), * next: t.union([t.nullType, EvenNumber]), // nullType first! * }), * ); * ``` * * @example * // Alternative: Provide explicit defaultValue for mutual recursion * ```ts * import * as t from 'ts-fortress'; * * type EvenNumber = Readonly<{ type: 'even'; next: OddNumber | null }>; * * type OddNumber = Readonly<{ type: 'odd'; next: EvenNumber | null }>; * * const OddNumber: t.Type = t.recursion('OddNumber', () => * t.record({ * type: t.literal('odd'), * next: t.union([t.nullType, {} as t.Type]), * }), * ); * * const EvenNumber: t.Type = t.recursion( * 'EvenNumber', * () => * t.record({ * type: t.literal('even'), * next: t.union([OddNumber, t.nullType]), // Order doesn't matter * }), * { defaultValue: { type: 'even' as const, next: null } }, // Explicit default * ); * ``` * * @example * // Mutually recursive types with optional fields * ```ts * import * as t from 'ts-fortress'; * * type EvenNumber = Readonly<{ type: 'even'; next?: OddNumber }>; * * type OddNumber = Readonly<{ type: 'odd'; next?: EvenNumber }>; * * // When using optional fields in mutually recursive types, * // use forceUndefinedDefault to avoid infinite loops when accessing defaultValue * const EvenNumber: t.Type = t.recursion('EvenNumber', () => * t.record({ * type: t.literal('even'), * next: t.optional(OddNumber, { forceUndefinedDefault: true }), * }), * ); * * const OddNumber: t.Type = t.recursion('OddNumber', () => * t.record({ * type: t.literal('odd'), * next: t.optional(EvenNumber, { forceUndefinedDefault: true }), * }), * ); * ``` * * @remarks * **Important Notes:** * - The `definition` function is called lazily on first use, enabling self-reference * - For mutually recursive types, accessing `defaultValue` may cause infinite loops * unless proper precautions are taken: * 1. Place terminal types (like `nullType`) first in union types, OR * 2. Provide an explicit `defaultValue` in options, OR * 3. Use `optional()` with `forceUndefinedDefault: true` for optional fields * - The type definition itself (validation, type checking) works fine regardless of * union ordering or defaultValue specification */ export const recursion = ( typeName: string, definition: () => Type, options?: Partial< Readonly<{ defaultValue: A; }> >, ): Type => { const getDefaultValue = memoizeFunction( (): A => options?.defaultValue ?? getInnerType().defaultValue, ); const getInnerType: () => Type = memoizeFunction(definition); const validate: Type['validate'] = (a) => getInnerType().validate(a); const is = createIsFn(validate); const fill: Type['fill'] = (a) => getInnerType().fill(a); const prune = (a: A): A => getInnerType().prune(a); const baseType: Type = { typeName, get defaultValue(): A { return getDefaultValue(); }, fill, prune, validate, is, assertIs: createAssertFn(validate), cast: createCastFn(validate), } as const; // Proxy to propagate RecordTypeInternals if the inner type has them return new Proxy(baseType, { get: (target, prop) => { if (prop === 'shapeStructure' || prop === 'excessProperty') { const inner = getInnerType(); if (hasRecordInternals(inner)) { return inner[prop]; } } if ( typeof prop === 'string' && isRecord(target) && hasKey(target, prop) ) { return target[prop as keyof typeof target]; } return undefined; }, has: (target, prop) => { if (prop === 'shapeStructure' || prop === 'excessProperty') { const inner = getInnerType(); return hasRecordInternals(inner); } return isRecord(target) && hasKey(target, prop); }, ownKeys: (target) => { const inner = getInnerType(); if (hasRecordInternals(inner)) { return [...Object.keys(target), 'shapeStructure', 'excessProperty']; } return Object.keys(target); }, getOwnPropertyDescriptor: (target, prop) => { if (prop === 'shapeStructure' || prop === 'excessProperty') { const inner = getInnerType(); if (hasRecordInternals(inner)) { return { enumerable: true, configurable: true, value: inner[prop], }; } } return Object.getOwnPropertyDescriptor(target, prop); }, }); };