jotai-state-tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,741 @@
1
+ /**
2
+ * Model Registry for Dynamic Model Registration
3
+ *
4
+ * Enables plugin architectures, code splitting, and lazy loading of models.
5
+ * Models can be registered at runtime and resolved dynamically.
6
+ *
7
+ * @example
8
+ * // In a plugin or lazy-loaded module:
9
+ * registerModel("UserProfile", UserProfileModel);
10
+ *
11
+ * // In main app, before plugin loads:
12
+ * const LazyUserProfile = types.lateModel("UserProfile");
13
+ *
14
+ * // Reference that resolves dynamically:
15
+ * const ref = types.dynamicReference("UserProfile", userId);
16
+ */
17
+
18
+ import type {
19
+ IAnyType,
20
+ IType,
21
+ IValidationContext,
22
+ IValidationResult,
23
+ } from "./types";
24
+ import {
25
+ $treenode,
26
+ getStateTreeNode,
27
+ StateTreeNode,
28
+ resolveIdentifier,
29
+ } from "./tree";
30
+
31
+ // ============================================================================
32
+ // Model Registry
33
+ // ============================================================================
34
+
35
+ /** Registered model entry */
36
+ interface ModelEntry {
37
+ type: IAnyType;
38
+ /** Optional metadata for the model */
39
+ metadata?: Record<string, unknown>;
40
+ }
41
+
42
+ /** Pending resolution callbacks for models not yet registered */
43
+ interface PendingResolution {
44
+ resolve: (type: IAnyType) => void;
45
+ reject: (error: Error) => void;
46
+ }
47
+
48
+ /** The global model registry */
49
+ const modelRegistry = new Map<string, ModelEntry>();
50
+
51
+ /** Pending resolutions for models that haven't been registered yet */
52
+ const pendingResolutions = new Map<string, PendingResolution[]>();
53
+
54
+ /** Listeners for model registration events */
55
+ const registrationListeners = new Set<(name: string, type: IAnyType) => void>();
56
+
57
+ /**
58
+ * Register a model type with a name for dynamic resolution.
59
+ *
60
+ * @param name - Unique name for the model
61
+ * @param type - The model type to register
62
+ * @param metadata - Optional metadata to associate with the model
63
+ * @throws Error if a model with the same name is already registered
64
+ *
65
+ * @example
66
+ * const UserModel = types.model("User", { id: types.identifier, name: types.string });
67
+ * registerModel("User", UserModel);
68
+ *
69
+ * // Later, in a plugin:
70
+ * registerModel("UserProfile", types.model("UserProfile", {
71
+ * user: types.reference(resolveModel("User")),
72
+ * bio: types.string
73
+ * }));
74
+ */
75
+ export function registerModel(
76
+ name: string,
77
+ type: IAnyType,
78
+ metadata?: Record<string, unknown>,
79
+ ): void {
80
+ if (modelRegistry.has(name)) {
81
+ throw new Error(
82
+ `[jotai-state-tree] Model "${name}" is already registered. ` +
83
+ `Use unregisterModel() first if you want to replace it.`,
84
+ );
85
+ }
86
+
87
+ modelRegistry.set(name, { type, metadata });
88
+
89
+ // Resolve any pending resolutions
90
+ const pending = pendingResolutions.get(name);
91
+ if (pending) {
92
+ pending.forEach(({ resolve }) => resolve(type));
93
+ pendingResolutions.delete(name);
94
+ }
95
+
96
+ // Notify listeners
97
+ registrationListeners.forEach((listener) => listener(name, type));
98
+ }
99
+
100
+ /**
101
+ * Unregister a model type.
102
+ *
103
+ * @param name - Name of the model to unregister
104
+ * @returns true if the model was unregistered, false if it wasn't registered
105
+ *
106
+ * @example
107
+ * unregisterModel("UserProfile");
108
+ */
109
+ export function unregisterModel(name: string): boolean {
110
+ return modelRegistry.delete(name);
111
+ }
112
+
113
+ /**
114
+ * Check if a model is registered.
115
+ *
116
+ * @param name - Name of the model to check
117
+ * @returns true if the model is registered
118
+ */
119
+ export function isModelRegistered(name: string): boolean {
120
+ return modelRegistry.has(name);
121
+ }
122
+
123
+ /**
124
+ * Resolve a model type by name (synchronous).
125
+ *
126
+ * @param name - Name of the model to resolve
127
+ * @returns The registered model type
128
+ * @throws Error if the model is not registered
129
+ *
130
+ * @example
131
+ * const UserModel = resolveModel("User");
132
+ * const instance = UserModel.create({ id: "1", name: "John" });
133
+ */
134
+ export function resolveModel<T extends IAnyType = IAnyType>(name: string): T {
135
+ const entry = modelRegistry.get(name);
136
+ if (!entry) {
137
+ throw new Error(
138
+ `[jotai-state-tree] Model "${name}" is not registered. ` +
139
+ `Make sure to call registerModel("${name}", YourModelType) before resolving.`,
140
+ );
141
+ }
142
+ return entry.type as T;
143
+ }
144
+
145
+ /**
146
+ * Try to resolve a model type by name (returns undefined if not registered).
147
+ *
148
+ * @param name - Name of the model to resolve
149
+ * @returns The registered model type or undefined
150
+ *
151
+ * @example
152
+ * const UserModel = tryResolveModel("User");
153
+ * if (UserModel) {
154
+ * const instance = UserModel.create({ id: "1", name: "John" });
155
+ * }
156
+ */
157
+ export function tryResolveModel<T extends IAnyType = IAnyType>(
158
+ name: string,
159
+ ): T | undefined {
160
+ return modelRegistry.get(name)?.type as T | undefined;
161
+ }
162
+
163
+ /**
164
+ * Resolve a model type by name (asynchronous).
165
+ * Waits for the model to be registered if it isn't already.
166
+ *
167
+ * @param name - Name of the model to resolve
168
+ * @param timeout - Optional timeout in milliseconds (default: 30000)
169
+ * @returns Promise that resolves to the model type
170
+ * @throws Error if timeout is reached
171
+ *
172
+ * @example
173
+ * // Wait for a plugin to register its model
174
+ * const UserProfileModel = await resolveModelAsync("UserProfile");
175
+ */
176
+ export function resolveModelAsync<T extends IAnyType = IAnyType>(
177
+ name: string,
178
+ timeout: number = 30000,
179
+ ): Promise<T> {
180
+ // Check if already registered
181
+ const entry = modelRegistry.get(name);
182
+ if (entry) {
183
+ return Promise.resolve(entry.type as T);
184
+ }
185
+
186
+ // Create pending resolution
187
+ return new Promise((resolve, reject) => {
188
+ const timeoutId = setTimeout(() => {
189
+ // Remove from pending
190
+ const pending = pendingResolutions.get(name);
191
+ if (pending) {
192
+ const index = pending.findIndex((p) => p.resolve === resolve);
193
+ if (index >= 0) {
194
+ pending.splice(index, 1);
195
+ if (pending.length === 0) {
196
+ pendingResolutions.delete(name);
197
+ }
198
+ }
199
+ }
200
+ reject(
201
+ new Error(
202
+ `[jotai-state-tree] Timeout waiting for model "${name}" to be registered`,
203
+ ),
204
+ );
205
+ }, timeout);
206
+
207
+ const wrappedResolve = (type: IAnyType) => {
208
+ clearTimeout(timeoutId);
209
+ resolve(type as T);
210
+ };
211
+
212
+ const wrappedReject = (error: Error) => {
213
+ clearTimeout(timeoutId);
214
+ reject(error);
215
+ };
216
+
217
+ if (!pendingResolutions.has(name)) {
218
+ pendingResolutions.set(name, []);
219
+ }
220
+ pendingResolutions.get(name)!.push({
221
+ resolve: wrappedResolve,
222
+ reject: wrappedReject,
223
+ });
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Get metadata for a registered model.
229
+ *
230
+ * @param name - Name of the model
231
+ * @returns The metadata or undefined
232
+ */
233
+ export function getModelMetadata(
234
+ name: string,
235
+ ): Record<string, unknown> | undefined {
236
+ return modelRegistry.get(name)?.metadata;
237
+ }
238
+
239
+ /**
240
+ * Get all registered model names.
241
+ *
242
+ * @returns Array of registered model names
243
+ */
244
+ export function getRegisteredModelNames(): string[] {
245
+ return Array.from(modelRegistry.keys());
246
+ }
247
+
248
+ /**
249
+ * Listen for model registration events.
250
+ *
251
+ * @param listener - Callback invoked when a model is registered
252
+ * @returns Disposer function to remove the listener
253
+ *
254
+ * @example
255
+ * const disposer = onModelRegistered((name, type) => {
256
+ * console.log(`Model ${name} was registered`);
257
+ * });
258
+ * // Later:
259
+ * disposer();
260
+ */
261
+ export function onModelRegistered(
262
+ listener: (name: string, type: IAnyType) => void,
263
+ ): () => void {
264
+ registrationListeners.add(listener);
265
+ return () => {
266
+ registrationListeners.delete(listener);
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Clear all registered models.
272
+ * Primarily useful for testing.
273
+ */
274
+ export function clearModelRegistry(): void {
275
+ modelRegistry.clear();
276
+ // Reject any pending resolutions
277
+ pendingResolutions.forEach((pending, name) => {
278
+ pending.forEach(({ reject }) => {
279
+ reject(
280
+ new Error(
281
+ `[jotai-state-tree] Model registry was cleared while waiting for "${name}"`,
282
+ ),
283
+ );
284
+ });
285
+ });
286
+ pendingResolutions.clear();
287
+ }
288
+
289
+ // ============================================================================
290
+ // Late Model Type (Registry-based lazy resolution)
291
+ // ============================================================================
292
+
293
+ /**
294
+ * Late model type that resolves from the registry.
295
+ * Unlike types.late(), this resolves by name from the global registry,
296
+ * allowing models to be registered after the type is defined.
297
+ */
298
+ class LateModelType<T extends IAnyType>
299
+ implements
300
+ IType<
301
+ T extends IType<infer C, unknown, unknown> ? C : unknown,
302
+ T extends IType<unknown, infer S, unknown> ? S : unknown,
303
+ T extends IType<unknown, unknown, infer I> ? I : unknown
304
+ >
305
+ {
306
+ readonly _kind = "lateModel" as const;
307
+ readonly _modelName: string;
308
+ readonly name: string;
309
+
310
+ readonly _C!: T extends IType<infer C, unknown, unknown> ? C : unknown;
311
+ readonly _S!: T extends IType<unknown, infer S, unknown> ? S : unknown;
312
+ readonly _T!: T extends IType<unknown, unknown, infer I> ? I : unknown;
313
+
314
+ private _resolvedType?: T;
315
+
316
+ constructor(modelName: string) {
317
+ this._modelName = modelName;
318
+ this.name = `lateModel("${modelName}")`;
319
+ }
320
+
321
+ private getType(): T {
322
+ if (!this._resolvedType) {
323
+ this._resolvedType = resolveModel<T>(this._modelName);
324
+ }
325
+ return this._resolvedType;
326
+ }
327
+
328
+ create(
329
+ snapshot?: T extends IType<infer C, unknown, unknown> ? C : unknown,
330
+ env?: unknown,
331
+ ): T extends IType<unknown, unknown, infer I> ? I : unknown {
332
+ return this.getType().create(snapshot, env) as T extends IType<
333
+ unknown,
334
+ unknown,
335
+ infer I
336
+ >
337
+ ? I
338
+ : unknown;
339
+ }
340
+
341
+ is(
342
+ value: unknown,
343
+ ): value is T extends IType<unknown, unknown, infer I> ? I : unknown {
344
+ return this.getType().is(value);
345
+ }
346
+
347
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
348
+ return this.getType().validate(value, context);
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Create a late-resolving type that looks up the model from the registry.
354
+ * This allows you to reference models that may not be registered yet.
355
+ *
356
+ * @param modelName - Name of the model in the registry
357
+ * @returns A type that resolves from the registry when used
358
+ *
359
+ * @example
360
+ * // Define a type that references a model not yet registered
361
+ * const PostStore = types.model("PostStore", {
362
+ * posts: types.array(types.lateModel("Post")),
363
+ * author: types.reference(types.lateModel("User"))
364
+ * });
365
+ *
366
+ * // Later, register the models (e.g., from a plugin)
367
+ * registerModel("Post", PostModel);
368
+ * registerModel("User", UserModel);
369
+ *
370
+ * // Now PostStore.create() will work
371
+ */
372
+ export function lateModel<T extends IAnyType = IAnyType>(
373
+ modelName: string,
374
+ ): IType<
375
+ T extends IType<infer C, unknown, unknown> ? C : unknown,
376
+ T extends IType<unknown, infer S, unknown> ? S : unknown,
377
+ T extends IType<unknown, unknown, infer I> ? I : unknown
378
+ > {
379
+ return new LateModelType<T>(modelName);
380
+ }
381
+
382
+ // ============================================================================
383
+ // Dynamic Reference Type (with custom resolvers)
384
+ // ============================================================================
385
+
386
+ /** Options for dynamic references */
387
+ export interface DynamicReferenceOptions<T extends IAnyType> {
388
+ /**
389
+ * Custom getter to resolve the reference.
390
+ * If not provided, uses the default identifier-based resolution.
391
+ *
392
+ * @param identifier - The stored identifier value
393
+ * @param parent - The parent node containing the reference
394
+ * @returns The resolved instance or undefined
395
+ */
396
+ get?: (
397
+ identifier: string | number,
398
+ parent: unknown,
399
+ ) => (T extends IType<unknown, unknown, infer I> ? I : unknown) | undefined;
400
+
401
+ /**
402
+ * Custom setter to extract the identifier from a value.
403
+ * If not provided, uses the identifier property of the value.
404
+ *
405
+ * @param value - The value being set
406
+ * @param parent - The parent node containing the reference
407
+ * @returns The identifier to store
408
+ */
409
+ set?: (
410
+ value: T extends IType<unknown, unknown, infer I> ? I : unknown,
411
+ parent: unknown,
412
+ ) => string | number;
413
+
414
+ /**
415
+ * Called when resolution fails.
416
+ * Can return a fallback value or throw an error.
417
+ */
418
+ onInvalidated?: (
419
+ identifier: string | number,
420
+ parent: unknown,
421
+ ) => (T extends IType<unknown, unknown, infer I> ? I : unknown) | undefined;
422
+ }
423
+
424
+ /**
425
+ * Dynamic reference type with custom resolution logic.
426
+ */
427
+ class DynamicReferenceType<T extends IAnyType>
428
+ implements
429
+ IType<
430
+ string | number,
431
+ string | number,
432
+ T extends IType<unknown, unknown, infer I> ? I : unknown
433
+ >
434
+ {
435
+ readonly _kind = "dynamicReference" as const;
436
+ readonly _modelName: string;
437
+ readonly _options: DynamicReferenceOptions<T>;
438
+ readonly name: string;
439
+
440
+ readonly _C!: string | number;
441
+ readonly _S!: string | number;
442
+ readonly _T!: T extends IType<unknown, unknown, infer I> ? I : unknown;
443
+
444
+ constructor(modelName: string, options: DynamicReferenceOptions<T> = {}) {
445
+ this._modelName = modelName;
446
+ this._options = options;
447
+ this.name = `dynamicReference("${modelName}")`;
448
+ }
449
+
450
+ create(
451
+ snapshot?: string | number,
452
+ env?: unknown,
453
+ ): T extends IType<unknown, unknown, infer I> ? I : unknown {
454
+ if (snapshot === undefined || snapshot === null) {
455
+ throw new Error(
456
+ `[jotai-state-tree] Cannot create dynamicReference with undefined/null identifier`,
457
+ );
458
+ }
459
+
460
+ // Create a proxy that resolves the reference on access
461
+ const self = this;
462
+
463
+ // For dynamic references, we return a getter-based proxy
464
+ // The actual resolution happens when properties are accessed
465
+ type InstanceType =
466
+ T extends IType<unknown, unknown, infer I> ? I : unknown;
467
+ const referenceProxy = new Proxy({} as object, {
468
+ get(_target, prop) {
469
+ // Resolve the actual target
470
+ const resolved = self.resolveReference(snapshot, null);
471
+ if (!resolved) {
472
+ if (self._options.onInvalidated) {
473
+ const fallback = self._options.onInvalidated(snapshot, null);
474
+ if (fallback) {
475
+ return (fallback as Record<string | symbol, unknown>)[prop];
476
+ }
477
+ }
478
+ throw new Error(
479
+ `[jotai-state-tree] Failed to resolve dynamicReference("${self._modelName}") with identifier "${snapshot}"`,
480
+ );
481
+ }
482
+ return (resolved as Record<string | symbol, unknown>)[prop];
483
+ },
484
+ has(_target, prop) {
485
+ const resolved = self.resolveReference(snapshot, null);
486
+ if (!resolved) return false;
487
+ return prop in (resolved as object);
488
+ },
489
+ ownKeys(_target) {
490
+ const resolved = self.resolveReference(snapshot, null);
491
+ if (!resolved) return [];
492
+ return Reflect.ownKeys(resolved as object);
493
+ },
494
+ getOwnPropertyDescriptor(_target, prop) {
495
+ const resolved = self.resolveReference(snapshot, null);
496
+ if (!resolved) return undefined;
497
+ return Object.getOwnPropertyDescriptor(resolved as object, prop);
498
+ },
499
+ });
500
+
501
+ return referenceProxy as InstanceType;
502
+ }
503
+
504
+ private resolveReference(
505
+ identifier: string | number,
506
+ parent: unknown,
507
+ ): (T extends IType<unknown, unknown, infer I> ? I : unknown) | undefined {
508
+ // Use custom getter if provided
509
+ if (this._options.get) {
510
+ return this._options.get(identifier, parent);
511
+ }
512
+
513
+ // Default: resolve from identifier registry
514
+ try {
515
+ const targetType = tryResolveModel(this._modelName);
516
+ if (!targetType) {
517
+ return undefined;
518
+ }
519
+ return resolveIdentifier(this._modelName, identifier) as T extends IType<
520
+ unknown,
521
+ unknown,
522
+ infer I
523
+ >
524
+ ? I
525
+ : unknown;
526
+ } catch {
527
+ return undefined;
528
+ }
529
+ }
530
+
531
+ is(
532
+ value: unknown,
533
+ ): value is T extends IType<unknown, unknown, infer I> ? I : unknown {
534
+ if (typeof value === "string" || typeof value === "number") {
535
+ return true; // Identifiers are valid
536
+ }
537
+ // Check if it's an instance of the target type
538
+ const targetType = tryResolveModel(this._modelName);
539
+ if (targetType) {
540
+ return targetType.is(value);
541
+ }
542
+ return false;
543
+ }
544
+
545
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
546
+ if (value === undefined || value === null) {
547
+ return {
548
+ valid: false,
549
+ errors: [
550
+ {
551
+ context,
552
+ value,
553
+ message: "Reference identifier cannot be undefined or null",
554
+ },
555
+ ],
556
+ };
557
+ }
558
+
559
+ if (typeof value !== "string" && typeof value !== "number") {
560
+ return {
561
+ valid: false,
562
+ errors: [
563
+ {
564
+ context,
565
+ value,
566
+ message: "Reference identifier must be a string or number",
567
+ },
568
+ ],
569
+ };
570
+ }
571
+
572
+ return { valid: true, errors: [] };
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Create a dynamic reference that resolves from the model registry.
578
+ * Supports custom get/set resolvers for advanced use cases like API fetching.
579
+ *
580
+ * @param modelName - Name of the model in the registry
581
+ * @param options - Optional custom resolution options
582
+ * @returns A reference type
583
+ *
584
+ * @example
585
+ * // Basic usage - resolves from registry
586
+ * const PostStore = types.model("PostStore", {
587
+ * author: types.dynamicReference("User")
588
+ * });
589
+ *
590
+ * // With custom resolver (e.g., API fetching)
591
+ * const PostStore = types.model("PostStore", {
592
+ * author: types.dynamicReference("User", {
593
+ * get(id, parent) {
594
+ * return userCache.get(id) ?? fetchUserSync(id);
595
+ * },
596
+ * set(user) {
597
+ * return user.id;
598
+ * },
599
+ * onInvalidated(id) {
600
+ * console.warn(`User ${id} not found`);
601
+ * return undefined;
602
+ * }
603
+ * })
604
+ * });
605
+ */
606
+ export function dynamicReference<T extends IAnyType = IAnyType>(
607
+ modelName: string,
608
+ options: DynamicReferenceOptions<T> = {},
609
+ ): IType<
610
+ string | number,
611
+ string | number,
612
+ T extends IType<unknown, unknown, infer I> ? I : unknown
613
+ > {
614
+ return new DynamicReferenceType<T>(modelName, options);
615
+ }
616
+
617
+ // ============================================================================
618
+ // Safe Dynamic Reference Type
619
+ // ============================================================================
620
+
621
+ /**
622
+ * Safe dynamic reference that returns undefined instead of throwing.
623
+ */
624
+ class SafeDynamicReferenceType<T extends IAnyType>
625
+ implements
626
+ IType<
627
+ string | number | undefined,
628
+ string | number | undefined,
629
+ (T extends IType<unknown, unknown, infer I> ? I : unknown) | undefined
630
+ >
631
+ {
632
+ readonly _kind = "safeDynamicReference" as const;
633
+ readonly _modelName: string;
634
+ readonly _options: DynamicReferenceOptions<T>;
635
+ readonly name: string;
636
+
637
+ readonly _C!: string | number | undefined;
638
+ readonly _S!: string | number | undefined;
639
+ readonly _T!:
640
+ | (T extends IType<unknown, unknown, infer I> ? I : unknown)
641
+ | undefined;
642
+
643
+ constructor(modelName: string, options: DynamicReferenceOptions<T> = {}) {
644
+ this._modelName = modelName;
645
+ this._options = options;
646
+ this.name = `safeDynamicReference("${modelName}")`;
647
+ }
648
+
649
+ create(
650
+ snapshot?: string | number,
651
+ env?: unknown,
652
+ ): (T extends IType<unknown, unknown, infer I> ? I : unknown) | undefined {
653
+ if (snapshot === undefined || snapshot === null) {
654
+ return undefined;
655
+ }
656
+
657
+ // Try to resolve, return undefined if not found
658
+ try {
659
+ if (this._options.get) {
660
+ return this._options.get(snapshot, null);
661
+ }
662
+
663
+ const targetType = tryResolveModel(this._modelName);
664
+ if (!targetType) {
665
+ return undefined;
666
+ }
667
+ return resolveIdentifier(this._modelName, snapshot) as T extends IType<
668
+ unknown,
669
+ unknown,
670
+ infer I
671
+ >
672
+ ? I
673
+ : unknown;
674
+ } catch {
675
+ if (this._options.onInvalidated) {
676
+ return this._options.onInvalidated(snapshot, null);
677
+ }
678
+ return undefined;
679
+ }
680
+ }
681
+
682
+ is(
683
+ value: unknown,
684
+ ): value is
685
+ | (T extends IType<unknown, unknown, infer I> ? I : unknown)
686
+ | undefined {
687
+ if (value === undefined) return true;
688
+ if (typeof value === "string" || typeof value === "number") return true;
689
+ const targetType = tryResolveModel(this._modelName);
690
+ if (targetType) {
691
+ return targetType.is(value);
692
+ }
693
+ return false;
694
+ }
695
+
696
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
697
+ if (value === undefined || value === null) {
698
+ return { valid: true, errors: [] };
699
+ }
700
+
701
+ if (typeof value !== "string" && typeof value !== "number") {
702
+ return {
703
+ valid: false,
704
+ errors: [
705
+ {
706
+ context,
707
+ value,
708
+ message:
709
+ "Reference identifier must be a string, number, or undefined",
710
+ },
711
+ ],
712
+ };
713
+ }
714
+
715
+ return { valid: true, errors: [] };
716
+ }
717
+ }
718
+
719
+ /**
720
+ * Create a safe dynamic reference that returns undefined instead of throwing.
721
+ *
722
+ * @param modelName - Name of the model in the registry
723
+ * @param options - Optional custom resolution options
724
+ * @returns A safe reference type
725
+ */
726
+ export function safeDynamicReference<T extends IAnyType = IAnyType>(
727
+ modelName: string,
728
+ options: DynamicReferenceOptions<T> = {},
729
+ ): IType<
730
+ string | number | undefined,
731
+ string | number | undefined,
732
+ (T extends IType<unknown, unknown, infer I> ? I : unknown) | undefined
733
+ > {
734
+ return new SafeDynamicReferenceType<T>(modelName, options);
735
+ }
736
+
737
+ // ============================================================================
738
+ // Type Exports
739
+ // ============================================================================
740
+
741
+ export type { ModelEntry };