···2020 steps:
2121 - uses: actions/checkout@v4
2222 - uses: ./.github/actions/prepare
2323- - run: pnpm format --list-different
2323+ - run: pnpm format
2424 type_check:
2525 name: Type Check
2626 runs-on: ubuntu-latest
+1-1
README.md
···33> [!WARNING]
44> this project is in the middle of active initial development and not ready for
55> use. there will be updates posted [here](https://bsky.app/profile/tylur.dev)
66-> if you'd like to follow along! or checkout the [todo.md](./todo.md)
66+> if you'd like to follow along!
7788
99
+257
notebook/skip-uniontotuple-optimization.md
···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
···11-# typed-lexicon - Development TODO
22-33-## Project Goal
44-55-Build a toolkit for writing ATProto lexicon JSON schemas in TypeScript that:
66-77-- Removes boilerplate and improves ergonomics
88-- Provides type hints for
99- [atproto type parameters](https://atproto.com/specs/lexicon#overview-of-types)
1010-- Infers TypeScript type definitions for data shapes to avoid duplication and
1111- skew
1212-- Includes methods and a CLI for generating JSON
1313-1414-## Files to Read
1515-1616-When working on this project, always reference:
1717-1818-1. **`lib.ts`** - Main implementation file with all `lx.*` methods
1919-2. **`tests/primitives.test.ts`** - Tests for all implemented types
2020-3. **`tests/base-case.test.ts`** - Example usage test
2121-4. **`README.md`** - Project direction and example usage
2222-2323-## Essential Resources
2424-2525-When implementing new lexicon types, fetch from:
2626-2727-- **Main spec**: https://atproto.com/specs/lexicon#overview-of-types
2828-- **Data model**: https://atproto.com/specs/data-model
2929-- **ATProto lexicon examples**:
3030- - https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/actor/defs.json
3131- (for `ref` examples)
3232- - https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/defs.json
3333- (for `token` examples)
3434-3535-## Implementation Status
3636-3737-- ✅ Initial implementation of field types returning json definitions
3838-- ✅ Bsky actor and feed test files created and passing
3939- (`tests/bsky-actor.test.ts` and `tests/bsky-feed.test.ts`)
4040-4141-## Todo
4242-4343-### CLI for JSON Emission
4444-4545-1. **Design CLI** - Determine command structure, flags, and output strategy
4646-2. **Create JSON emission logic** - Traverse lexicon objects and serialize to
4747- formatted JSON
4848-3. **Add file I/O** - Read TypeScript lexicon files, write JSON output files
4949-4. **Write CLI documentation** - Usage examples, flag reference, common
5050- workflows
5151-5252-### Type Inference System
5353-5454-Infer TypeScript types from lexicon definitions
5555-5656-### `validate()`
5757-5858-validate any lexicon schema json at runtime