···11-# Skip UnionToTuple When Empty - Optimization Notes
22-33-## Starting Point
44-55-**Initial Benchmark Results (from todo.md):**
66-77-- Simple object (2 properties): **578 instantiations** (baseline: 62, 9.3x over)
88-- Complex nested (3 defs): **971 instantiations** (baseline: 124, 7.8x over)
99-1010-**Target:** Reduce to ~200-250 instantiations for simple case
1111-1212-## Before This Optimization
1313-1414-Previous optimizations had already been applied (likely "Make Prettify Lazy"):
1515-1616-- Simple object: **275 instantiations**
1717-- Complex nested: **542 instantiations**
1818-1919-## The Problem
2020-2121-The `ObjectResult` and `ParamsResult` types were computing `RequiredKeys<T>` and `NullableKeys<T>` as intermediate type parameters:
2222-2323-```typescript
2424-type ObjectResult<
2525- T extends ObjectProperties,
2626- R = RequiredKeys<T>, // ← Intermediate type parameter
2727- N = NullableKeys<T>, // ← Intermediate type parameter
2828-> = {
2929- type: "object";
3030- properties: {...};
3131-} & ([R] extends [never] ? {} : { required: UnionToTuple<R> })
3232- & ([N] extends [never] ? {} : { nullable: UnionToTuple<N> });
3333-```
3434-3535-While the conditional checks prevented calling `UnionToTuple` when keys were `never`, TypeScript still had to instantiate the intermediate type parameters `R` and `N`, adding overhead.
3636-3737-## What We Tried
3838-3939-### Attempt 1: Remove Intermediate Type Parameters ✅ SUCCESS
4040-4141-**Change:** Inline the `RequiredKeys<T>` and `NullableKeys<T>` calls directly into the conditional checks, removing the intermediate type parameter assignments.
4242-4343-```typescript
4444-type ObjectResult<T extends ObjectProperties> = {
4545- type: "object";
4646- properties: {...};
4747-} & ([RequiredKeys<T>] extends [never]
4848- ? {}
4949- : { required: UnionToTuple<RequiredKeys<T>> })
5050- & ([NullableKeys<T>] extends [never]
5151- ? {}
5252- : { nullable: UnionToTuple<NullableKeys<T>> });
5353-```
5454-5555-**Results:**
5656-5757-- Simple object: 275 → **244 instantiations** (31 saved, ~11% improvement)
5858-- Complex nested: 542 → **507 instantiations** (35 saved, ~6% improvement)
5959-6060-**Why it worked:** Removing intermediate type parameter assignments reduced the number of type instantiations. TypeScript now evaluates the key extraction inline within conditionals, which is more efficient than creating separate type aliases.
6161-6262-### Attempt 2: Extract Helper Types ❌ FAILED
6363-6464-**Change:** Created `AddRequiredField` and `AddNullableField` helper types to encapsulate the conditional logic:
6565-6666-```typescript
6767-type AddRequiredField<R> = [R] extends [never]
6868- ? {}
6969- : { required: UnionToTuple<R> };
7070-7171-type AddNullableField<N> = [N] extends [never]
7272- ? {}
7373- : { nullable: UnionToTuple<N> };
7474-7575-type ObjectResult<T extends ObjectProperties> = {
7676- type: "object";
7777- properties: {...};
7878-} & AddRequiredField<RequiredKeys<T>>
7979- & AddNullableField<NullableKeys<T>>;
8080-```
8181-8282-**Results:**
8383-8484-- Simple object: 244 → **263 instantiations** (19 worse, regressed)
8585-- Complex nested: 507 → **506 instantiations** (1 better, negligible)
8686-8787-**Why it failed:** The helper types added additional type instantiation overhead that outweighed any benefits from code organization. Each helper type invocation created extra work for TypeScript's type checker.
8888-8989-**Action taken:** Reverted to Attempt 1.
9090-9191-## Final Results
9292-9393-**Benchmark after this optimization:**
9494-9595-- Simple object: **244 instantiations** (down from 275, saved 31)
9696-- Complex nested: **507 instantiations** (down from 542, saved 35)
9797-9898-**Total progress from original todo.md baseline:**
9999-100100-- Simple object: **578 → 244** (334 saved, 57.8% improvement) ✅ **GOAL MET** (target: 200-250)
101101-- Complex nested: **971 → 507** (464 saved, 47.8% improvement) ⚠️ Still above 400 target
102102-103103-## Files Modified
104104-105105-- `src/lib.ts`:
106106- - `ObjectResult<T>` type (lines 212-225)
107107- - `ParamsResult<T>` type (lines 237-245)
108108-109109-## Validation
110110-111111-- ✅ All 172 tests passing (`pnpm test`)
112112-- ✅ Type checking passes (`pnpm tsc`)
113113-- ✅ No runtime behavior changes (types erase at runtime)
114114-- ✅ IDE autocomplete still works with clean type display
115115-116116-## Key Learnings
117117-118118-1. **Intermediate type parameters have a cost**: Even when they're just aliases, TypeScript must instantiate them.
119119-120120-2. **Inline conditionals can be more efficient**: Computing values inline within conditional types can reduce instantiation count compared to pre-computing and storing in type parameters.
121121-122122-3. **Helper types aren't always helpful**: While helper types improve code organization, they can add overhead. Always benchmark after introducing abstractions.
123123-124124-4. **Small changes add up**: A 31-instantiation reduction (11%) might seem modest, but combined with other optimizations, we achieved a 57.8% total improvement.
125125-126126----
127127-128128-## Further Optimization Attempts (Post-Initial Success)
129129-130130-### Attempt 3: Reduce InferObject Intersections (4 → 2) ❌ FAILED
131131-132132-**Change:** Attempted to reduce the number of intersected mapped types in `InferObject` from 4 to 2 by combining key categories:
133133-134134-```typescript
135135-type InferObject<...> = Prettify<
136136- T extends { properties: any }
137137- ? {
138138- // All REQUIRED keys (with and without nullable)
139139- -readonly [K in keyof Props as K extends Required & string ? K : never]-?:
140140- K extends NullableAndRequired
141141- ? InferType<Props[K]> | null
142142- : InferType<Props[K]>;
143143- } & {
144144- // All OPTIONAL keys (normal and nullable-only)
145145- -readonly [K in keyof Props as K extends Exclude<keyof Props & string, Required> ? K : never]?:
146146- K extends Nullable
147147- ? InferType<Props[K]> | null
148148- : InferType<Props[K]>;
149149- }
150150- : {}
151151->;
152152-```
153153-154154-**Results:**
155155-156156-- Simple object: **244 instantiations** (unchanged)
157157-- Complex nested: **507 instantiations** (unchanged)
158158-159159-**Why it failed:**
160160-161161-- The number of intersections wasn't the bottleneck
162162-- TypeScript still evaluates the same number of conditional checks
163163-- Property ordering changed (required first, then optional), breaking snapshot tests
164164-- No performance benefit to justify the breaking change
165165-166166-**Action taken:** Reverted.
167167-168168-### Attempt 4: Inline Type Parameters in InferObject ❌ FAILED
169169-170170-**Change:** Removed all intermediate type parameters from `InferObject`, similar to what worked for `ObjectResult`:
171171-172172-```typescript
173173-type InferObject<T> = Prettify<
174174- T extends { properties: infer P }
175175- ? {
176176- -readonly [K in "properties" extends keyof T
177177- ? Exclude<keyof T["properties"], (GetRequired<T> & string) | (GetNullable<T> & string)> & string
178178- : never]?: InferType<P[K & keyof P]>;
179179- } & {
180180- -readonly [K in Exclude<GetRequired<T> & string, ...>]-?: InferType<P[K & keyof P]>;
181181- } & ...
182182- : {}
183183->;
184184-```
185185-186186-**Results:**
187187-188188-- Simple object: **244 instantiations** (unchanged)
189189-- Complex nested: **507 instantiations** (unchanged)
190190-191191-**Why it failed:**
192192-193193-- Unlike `ObjectResult` (which is only evaluated at definition time), `InferObject` is called recursively during type inference
194194-- The intermediate type parameters are likely cached/memoized by TypeScript during recursive evaluation
195195-- Inlining forces re-computation of the same values multiple times in each mapped type key
196196-- Tests passed but no performance improvement
197197-198198-**Action taken:** Reverted.
199199-200200-## Updated Key Learnings
201201-202202-5. **Context matters for optimizations**: What works in one context (removing intermediate params in `ObjectResult`) may not work in another (`InferObject`). The recursive nature of type inference behaves differently than one-time type construction.
203203-204204-6. **Intersection count isn't always the bottleneck**: Reducing from 4 to 2 intersections had zero impact, suggesting the real cost is elsewhere (likely `Prettify` at every nesting level or the recursive `InferType` calls).
205205-206206-7. **TypeScript may optimize intermediate parameters**: In recursive scenarios, intermediate type parameters might be cached, making inlining counterproductive.
207207-208208-### Attempt 5: Reorder InferType Dispatch Chain ❌ FAILED
209209-210210-**Change:** Reordered the `InferType` conditional chain to prioritize the most commonly used types:
211211-212212-**New order:**
213213-214214-1. object (most common container)
215215-2. string (most common primitive)
216216-3. ref (common for schema references)
217217-4. array (common for collections)
218218-5. union (common for polymorphic types)
219219-6. integer, boolean (other common primitives)
220220-7. record, params, null, token, unknown, bytes, cid-link, blob (less common)
221221-222222-**Previous order:**
223223-224224-1. record, object, array, params, union, token, ref, unknown, null, boolean, integer, string, bytes, cid-link, blob
225225-226226-```typescript
227227-type InferType<T> = T extends { type: "object" }
228228- ? InferObject<T>
229229- : T extends { type: "string" }
230230- ? string
231231- : T extends { type: "ref" }
232232- ? InferRef<T>
233233- : T extends { type: "array" }
234234- ? InferArray<T>
235235- // ... rest of the chain
236236-```
237237-238238-**Results:**
239239-240240-- Simple object: **244 instantiations** (unchanged)
241241-- Complex nested: **507 instantiations** (unchanged)
242242-243243-**Why it failed:**
244244-245245-- TypeScript's type checker likely doesn't evaluate conditional chains linearly
246246-- The order of conditionals has no impact on performance
247247-- TypeScript may cache or optimize type instantiations internally regardless of order
248248-- Tests pass, proving functional correctness, but zero performance benefit
249249-250250-**Action taken:** Kept the new order (it's more readable with common types first), but no performance gain.
251251-252252-## Next Potential Optimizations to Try
253253-254254-1. **Optimize `Prettify` itself** - Since it's called at every nesting level, making it more efficient could have cascading benefits
255255-2. **Combine `GetRequired` and `GetNullable`** - Extract both in a single pass to reduce type instantiations
256256-3. **Cache commonly used helper types** - Though previous attempts suggest this might not help
257257-4. **Reduce Prettify calls** - Only call Prettify at the outermost level, not at every nesting
+43-4
src/infer.ts
···9494 : unknown
9595 : unknown;
96969797-type InferDefs<T extends Record<string, unknown>> = {
9898- -readonly [K in keyof T]: InferType<T[K]>;
9999-};
9797+/**
9898+ * Recursively replaces stub references in a type with their actual definitions.
9999+ * Detects circular references and missing references, returning string literal error messages.
100100+ */
101101+type ReplaceRefsInType<T, Defs, Visited = never> =
102102+ // Check if this is a ref stub type (has $type starting with #)
103103+ T extends { $type: `#${infer DefName}` }
104104+ ? DefName extends keyof Defs
105105+ ? // Check for circular reference
106106+ DefName extends Visited
107107+ ? `[Circular reference detected: #${DefName}]`
108108+ : // Recursively resolve the ref and preserve the $type marker
109109+ Prettify<
110110+ ReplaceRefsInType<Defs[DefName], Defs, Visited | DefName> & {
111111+ $type: T["$type"];
112112+ }
113113+ >
114114+ : // Reference not found in definitions
115115+ `[Reference not found: #${DefName}]`
116116+ : // Handle arrays (but not Uint8Array or other typed arrays)
117117+ T extends Uint8Array | Blob
118118+ ? T
119119+ : T extends readonly (infer Item)[]
120120+ ? ReplaceRefsInType<Item, Defs, Visited>[]
121121+ : // Handle plain objects (exclude built-in types and functions)
122122+ T extends object
123123+ ? T extends (...args: unknown[]) => unknown
124124+ ? T
125125+ : { [K in keyof T]: ReplaceRefsInType<T[K], Defs, Visited> }
126126+ : // Primitives pass through unchanged
127127+ T;
100128129129+/**
130130+ * Infers the TypeScript type for a lexicon namespace, returning only the 'main' definition
131131+ * with all local refs (#user, #post, etc.) resolved to their actual types.
132132+ */
101133export type Infer<T extends { id: string; defs: Record<string, unknown> }> =
102102- Prettify<InferDefs<T["defs"]>>;
134134+ Prettify<
135135+ "main" extends keyof T["defs"]
136136+ ? { $type: T["id"] } & ReplaceRefsInType<
137137+ InferType<T["defs"]["main"]>,
138138+ { [K in keyof T["defs"]]: InferType<T["defs"][K]> }
139139+ >
140140+ : never
141141+ >;