diff --git a/.changeset/twenty-olives-sit.md b/.changeset/twenty-olives-sit.md
new file mode 100644
index 00000000..c72c2260
--- /dev/null
+++ b/.changeset/twenty-olives-sit.md
@@ -0,0 +1,5 @@
+---
+"@traversable/zod": patch
+---
+
+docs(zod): adds docs for `zx.makeLens`
diff --git a/packages/zod/README.md b/packages/zod/README.md
index 9ca780f2..d4126387 100644
--- a/packages/zod/README.md
+++ b/packages/zod/README.md
@@ -3,7 +3,7 @@
@traversable/zod or zx is an expansion pack for zod.
-
+
The primary abstraction that powers zx is an obscure, if surprisingly useful idea from category theory called recursion schemes (and don't worry -- I promise you don't need any math to use zx π).
@@ -120,7 +120,7 @@ const MySchema = z.object({
b: z.object({
c: z.string(),
d: z.array(z.boolean())
- })
+ })
})
const defaultOne = zx.defaultValue(MySchema)
@@ -233,91 +233,293 @@ console.log(
#### `zx.makeLens`
-- Example
+`zx.makeLens` accepts a zod schema (classic, v4) as its first argument, and a
+"selector function" as its second argument.
+
+Use the selector function to build up a lens via a series of property accesses.
+
+Let's look at a few examples to make things more concrete.
+
+##### Example #1: Lens
+
+For our first example, let's create a lens that focuses on a structure's `"a[0]"` path:
```typescript
import { z } from 'zod/v4'
import { zx } from "@traversable/zod"
-////////////////////
-/// example #1 ///
-////////////////////
+//////////////////////////
+/// example #1: Lens ///
+//////////////////////////
-let schema_01 = z.tuple([z.string(), z.bigint()])
-let lens_01 = zx.makeLens(schema_01)
+const Schema = z.object({ a: z.tuple([z.string(), z.bigint()]) })
-lens_01
-// ^? let lens_01: zx.SchemaLens<[string, bigint], string>
+// Use autocompletion to "select" what you want to focus:
+// ββββββ
+const Lens = zx.makeLens(Schema, $ => $.a[0])
-let get_01 = lens_01.get(['', 0n])
-// ^? let get_01: string
-console.log(get_01) // => ''
+Lens
+// ^? const Lens: zx.Lens<{ a: [string, bigint] }, string>
+// π___________________π π____π
+// structure focus
-let set_01 = lens_01.set('hey', ['', 0n])
-// ^? let set_01: [string, bigint]
-console.log(set_01) // => ['hey', 0n]
+// Lenses have 3 properties:
-let modify_01 = lens_01.modify((s) => string.length > 0, ['', 0n])
-// ^? let modify_01: [boolean, bigint]
-console.log(modify_01) // => [false, 0n]
+///////////////
+// #1:
+// Lens.get -- Given a structure,
+// returns the focus
+const ex_01 = Lens.get({ a: ['hi', 0n] })
+// π_____________π
+// structure
-////////////////////
-/// example #2 ///
-////////////////////
+console.log(ex_01) // => "hi"
+// ππ
+// focus
-let schema_02 = z.union([
- z.object({
- tag: z.literal('ONE'),
- ghi: z.number(),
- }),
- z.object({
- tag: z.literal('TWO'),
- jkl: z.boolean(),
- })
+
+///////////////
+// #2:
+// Lens.set -- Given a new focus and a structure,
+// sets the new focus & returns the structure
+
+const ex_02 = Lens.set(`hey, ho, let's go`, { a: ['', 0n] })
+// π_______________π π___________π
+// new focus structure
+
+console.log(ex_02) // => { a: ["hey, ho, let's go", 0n] }
+// π_______________π
+// new focus
+
+
+/////////////////
+// #3:
+// Lens.modify -- Given a "modify" callback and a structure,
+// applies the callback to the focus & returns the structure
+
+const ex_03 = Lens.modify((str) => str.toUpperCase(), { a: [`hey, ho`, 0n] })
+// π_______________________π π__________________π
+// callback structure
+
+console.log(ex_03) // => { a: ["HEY, HO", 0n] }
+// π_____π
+// new focus
+
+// Note that if your callback changes the focus type,
+// that will be reflected in the return type as well:
+
+const ex_04 = Lens.modify((str) => str.length > 0, { a: ['', 0n] })
+// π____________________π π___________π
+// callback structure
+
+console.log(ex_04) // => { a: [false, 0n] }
+// ^? const ex_04: { a: [boolean, bigint] }
+// π_____π
+// new focus
+```
+
+##### Example #2: Prism
+
+When you use `zx.makeLens` on a __union type__, you get back a different kind
+of lens called a __prism__.
+
+Let's see how prisms differ from lenses:
+
+```typescript
+import { z } from 'zod/v4'
+import { zx } from "@traversable/zod"
+
+///////////////////////////
+/// example #2: Prism ///
+///////////////////////////
+
+const Schema = z.union([
+ z.object({ tag: z.literal('ONE'), ghi: z.number() }),
+ z.object({ tag: z.literal('TWO') })
])
-let lens_02 = zx.makeLens(
- schema,
- $ => $.κONE.ghi
-)
+// Let's focus on the first union member's "ghi" property.
+
+// If a discriminant can be inferred, autocompletion allows
+// you to select that member by its discriminant,
+// prefixed by `κ`:
+//
+// βββββ
+const Prism = zx.makeLens(Schema, $ => $.κONE.ghi)
+
+Prism
+// ^? Prism: zx.Prism<{ tag: "ONE", ghi: number } | { tag: "TWO" }, number | undefined>
+// π________________________________________π π________________π
+// structure focus
+
+// Prisms have the same 3 properties as lenses,
+// but they behave like **pattern matchers**
+// instead of _property accessors_
+
+///////////////
+// #1:
+// Prism.get -- Given a matching structure,
+// returns the focus
+
+const ex_01 = Prism.get({ tag: 'ONE', ghi: 123 })
+// π____________________π
+// structure
+
+console.log(ex_01) // => 123
+// πππ
+// focus
+
+// Prism.get -- If the match fails,
+// returns undefined
+
+const ex_02 = Prism.get({ tag: 'TWO' })
+// π___________π
+// structure
+
+console.log(ex_02) // => undefined
+// πππ
+// no match
+
+
+///////////////
+// #2:
+// Prism.set -- Given a new focus and a matching structure,
+// sets the new focus & returns the structure
+
+const ex_03 = Prism.set(9_000, { tag: 'ONE', ghi: 123 })
+// π___π π____________________π
+// new focus structure
-lens_02
-// ^? let lens_02: zx.SchemaLens<
-// | { tag: "ONE", ghi: number }
-// | { tag: "TWO", jkl: boolean },
-// number | undefined
-// >
-
-let get_02A = lens_02.get({ tag: 'ONE', ghi: 0 })
-// ^? let get_02A: number | undefined
-console.log(get_02A) // => 0
-
-let get_02B = lens_02.get({ tag: 'TWO', jkl: true })
-// ^? let get_02B: number | undefined
-console.log(get_02B) // => undefined
-
-let set_02A = lens_02.set(9000, { tag: 'ONE', ghi: 0 })
-// ^? let set_02A: { tag: "ONE", ghi: number } | { tag: "TWO", jkl: boolean }
-console.log(set_02A) // => { tag: 'ONE', ghi: 9000 }
-
-let set_02B = lens_02.set(9000, { tag: 'TWO', jkl: true })
-// ^? let set_02B: { tag: "ONE", ghi: number } | { tag: "TWO", jkl: boolean }
-console.log(set_02B) // => { tag: 'TWO', jkl: true }
-
-let modify_02A = lens_02.modify((n) => [n, n] as const, { tag: 'ONE', ghi: 0 })
-// ^? let modify_02A:
-// | { tag: "ONE", ghi: readonly [number, number] }
-// | { tag: "TWO", jkl: boolean }
-console.log(modify_02A) // => { tag: 'ONE', ghi: [0, 0] }
-
-let modify_02B = lens_02.modify((n) => [n, n] as const, { tag: 'TWO', jkl: true })
-// ^? let modify_02B:
-// | { tag: "ONE", ghi: readonly [number, number] }
-// | { tag: "TWO", jkl: boolean }
-console.log(modify_02B) // => { tag: 'TWO', jkl: true }
+console.log(ex_03) // => { tag: 'ONE', ghi: 9000 }
+// π__π
+// new focus
+
+// Prism.set -- If the match fails,
+// returns the structure unchanged
+
+const ex_04 = Prism.set(9000, { tag: 'TWO' })
+
+console.log(ex_04) // => { tag: 'TWO' }
+// π__________π
+// no match
+
+
+//////////////////
+// #3:
+// Prism.modify -- Given a "modify" callback and a matching structure,
+// applies the callback to the focus & returns the structure
+
+// Just like with lenses, if your callback changes the focus type,
+// that will be reflected in the return type:
+
+const ex_05 = Prism.modify((n) => [n, n], { tag: 'ONE', ghi: 123 })
+// π___________π π____________________π
+// callback structure
+
+console.log(ex_05) // => { tag: 'ONE', ghi: [123, 123] }
+// ^? const ex_05: { tag: "ONE", ghi: number[] } | { tag: "TWO" }
+
+// Prism.modify -- If the match fails,
+// returns the structure unchanged
+
+const ex_06 = Prism.modify((n) => n + 1, { tag: 'TWO' })
+// π__________π π___________π
+// callback structure
+
+console.log(ex_06) // => { tag: 'TWO' }
+// ^? const ex_06: { tag: "ONE", ghi: number } | { tag: "TWO" }
+```
+
+##### Example #3: Traversal
+
+When you use `zx.makeLens` on a __collection type__ (such as `z.array` or `z.record`),
+you get back a different kind of lens called a __traversal__.
+
+Let's see how traversals differ from lenses and prisms:
+
+```typescript
+import { z } from 'zod/v4'
+import { zx } from "@traversable/zod"
+
+///////////////////////////////
+/// example #3: Traversal ///
+///////////////////////////////
+
+const Schema = z.object({
+ a: z.array(
+ z.object({
+ b: z.number(),
+ c: z.string()
+ })
+ )
+})
+
+// Let's focus on the `"b"` property of each of the elements of the structure's `"a"` property:
+
+// To indicate that you want to traverse the array,
+// autocomplete the `α£κΈκΈ` field:
+// ββ
+const Traversal = zx.makeLens(Schema, $ => $ => $.a.α£κΈκΈ.b)
+
+
+Traversal
+// ^? Traversal: zx.Traversal<{ a: { b: number, c: string }[] }, number>
+// π_____________________________π π____π
+// structure focus
+
+// Traversals have the same 3 properties as lenses and prisms,
+// but they behave like **for-of loops**
+// instead of _property accessors_ or _patterns matchers_
+
+
+///////////////
+// #1:
+// Traversal.get -- Given a matching structure,
+// returns all of the focuses
+
+const ex_01 = Traversal.get({ a: [{ b: 0, c: '' }, { b: 1, c: '' }] })
+// π_____________________________________π
+// structure
+
+console.log(ex_01) // => [0, 1]
+// π__π
+// focus
+
+
+///////////////
+// #2:
+// Traversal.set -- Given a new focus and a matching structure, sets all of the elements
+// of the collection to the new focus & returns the structure
+
+const ex_02 = Traversal.set(9_000, { a: [{ b: 0, c: '' }, { b: 1, c: '' }] })
+// π___π π_____________________________________π
+// new focus structure
+
+console.log(ex_02) // => { a: [{ b: 9000, c: '' }, { b: 9000, c: '' }] }
+// π__π π__π
+// new focus new focus
+
+
+//////////////////
+// #3:
+// Traversal.modify -- Given a "modify" callback and a matching structure,
+// applies the callback to _each_ focus & returns the structure
+
+// Just like with lenses & prisms, if your callback changes the focus type,
+// that will be reflected in the return type:
+
+const ex_03 = Traversal.modify((n) => [n, n + 1], { a: [{ b: 0, c: '' }, { b: 1, c: '' }] })
+// π______________π π_____________________________________π
+// callback structure
+
+console.log(ex_03) // => { a: [{ b: [0, 1], c: '' }, { b: [1, 2], c: '' }] }
+// ^? const ex_03: { a: { b: number[], c: string }[] }
+// π______π
+// new focus
```
+
## Advanced Features
### Combinators
diff --git a/packages/zod/src/lens.ts b/packages/zod/src/lens.ts
index 15e35299..dc4a190d 100644
--- a/packages/zod/src/lens.ts
+++ b/packages/zod/src/lens.ts
@@ -1,7 +1,6 @@
-// import type { z } from 'zod/v4'
import { z } from 'zod/v4'
-import type { Force, Key, newtype, Showable } from '@traversable/registry'
+import type { Key, newtype, Showable } from '@traversable/registry'
import {
Array_from,
Array_isArray,
@@ -97,9 +96,6 @@ declare namespace Z {
= { _zod: { def: { type: unknown } } }
> { _zod: { def: { type: T['_zod']['def']['type'] } }, _output: unknown }
//
- /**
- * TODO: see if `z.set`'s valueType has been added to `_zod.def.valueType` in zod v3.25.0
- */
interface Set { _zod: { def: { type: 'set' } }, _output: unknown }
interface Enum<
T extends
@@ -312,7 +308,7 @@ const Invariant = {
TaggedSchemaNotFound(Cmd) { throw Error('8' + String(Cmd)) },
PropertyNotFound(Cmd) { throw Error('9' + String(Cmd)) },
IndexNotFound(Cmd) { throw Error('10' + String(Cmd)) },
-} satisfies Record never>
+} satisfies globalThis.Record never>
const previousOpticWasLens = (acc: Profunctor.Optic[]) => acc[acc.length - 1]?.[symbol.tag] === 'Lens'
@@ -442,16 +438,16 @@ function interpreter(type: T, ..._path: (keyof any)[]) {
// console.log()
const tag = optic[symbol.tag]
- const get = Get[tag](optic) as SchemaLens['get']
- const set = Set[tag](optic) as SchemaLens['set']
- const modify = Modify[tag](optic) as SchemaLens['modify']
+ const get = Get[tag](optic) as Lens['get']
+ const set = Set[tag](optic) as Lens['set']
+ const modify = Modify[tag](optic) as Lens['modify']
return {
get,
set,
modify,
type: tag,
- } // satisfies SchemaLens, Z.infer, []>
+ } // satisfies Lens, Z.infer, []>
}
// const optics = path.reduce(
@@ -552,37 +548,33 @@ interface Proxy_optional {
[DSL.chainOptional]: Proxy
[DSL.coalesceOptional]: Proxy], KS>
[symbol.type]: T
- [symbol.path]: [...KS, symbol.optional]
+ [symbol.path]: KS
}
interface Proxy_array {
[DSL.traverseArray]: Proxy
[symbol.type]: T
- [symbol.path]: [...KS, array: number]
+ [symbol.path]: KS
// [symbol.path]: [...KS, symbol.array]
}
-type Proxy_record
+type RecordType
= S[0] extends { enum: { [x: string | number]: string | number } }
- ? never | Proxy_finiteRecord
- : never | Proxy_nonfiniteRecord
+ ? never | Proxy_finiteRecord
+ : never | Proxy_nonfiniteRecord
interface Proxy_nonfiniteRecord {
- [DSL.traverseRecord]: Proxy
- // [DSL.traverseRecord]: Proxy
+ [DSL.traverseRecord]: Proxy
[symbol.type]: T
- [symbol.path]: [...KS, nonfiniteRecord: string]
- // [symbol.path]: [...KS, string]
+ [symbol.path]: KS
}
interface Proxy_finiteRecord extends newtype<
{ [K in S[0]['enum'][keyof S[0]['enum']]]: Proxy }
> {
- [DSL.traverseRecord]: Proxy
- // [DSL.traverseRecord]: Proxy
+ [DSL.traverseRecord]: Proxy
[symbol.type]: T
- [symbol.path]: [...KS, finiteRecord: string]
- // [symbol.path]: [...KS, symbol.record]
+ [symbol.path]: KS
}
interface Proxy_primitive {
@@ -596,7 +588,7 @@ interface Proxy_set {
[symbol.path]: KS
}
-type Union<
+type UnionType<
T,
S extends [any],
KS extends (keyof any)[],
@@ -604,25 +596,26 @@ type Union<
Disc extends keyof any = keyof _[number]['_zod']['def']['shape'],
Tag extends keyof any = _[number]['_zod']['def']['shape'][Disc]['_output']
> = [Disc] extends [never]
- ? never | Proxy_union
+ ? never | Proxy_union
: never | Disjoint<
Tag extends Tag
? [
Tag,
Extract<_[number]['_zod']['def']['shape'], Record>,
- Extract>
+ Extract>,
+ Disc
]
: never,
- KS
+ [...KS, disjoint: symbol.disjoint]
>
-interface Proxy_union extends newtype<
+interface Proxy_union extends newtype<
{ [I in Extract as `${GlobalDSL['unionPrefix']}${I}`]: Proxy }
-> { [symbol.type]: T }
+> { [symbol.type]: T, [symbol.path]: KS }
-type Disjoint = never | Proxy_disjointUnion<
+type Disjoint = never | Proxy_disjointUnion<
U[2],
- { [M in U as `${GlobalDSL['unionPrefix']}${Key}`]: Proxy_object<[M[2], { [K in keyof M[1]]: M[1][K] }], KS> },
+ { [M in U as `${GlobalDSL['unionPrefix']}${Key}`]: Proxy_object<[M[2], { [K in keyof M[1]]: M[1][K] }], [...KS, U[3], M[0]]> },
KS
>
@@ -633,15 +626,31 @@ interface Proxy_disjointUnion extends
type Proxy
= [S] extends [Z.Object] ? Proxy_object<[T[0]['_output'], S['_zod']['def']['shape']], KS>
- : [S] extends [Z.Optional] ? Proxy_optional
+ : [S] extends [Z.Optional] ? Proxy_optional
: [S] extends [Z.Tuple] ? Proxy_tuple
- : [S] extends [Z.Union] ? Union
- : [S] extends [Z.Array] ? Proxy_array
- : [S] extends [Z.Record] ? Proxy_record
- : [S] extends [Z.Set] ? Proxy_set ? V : never, KS>
+ : [S] extends [Z.Union] ? UnionType
+ : [S] extends [Z.Array] ? Proxy_array
+ : [S] extends [Z.Record] ? RecordType
+ : [S] extends [Z.Set] ? Proxy_set ? V : never, [...KS, symbol.set]>
: Proxy_primitive
-export interface SchemaLens {
+type MakeLens
+ = symbol.array extends KS[number] ? Traversal
+ : symbol.record extends KS[number] ? Traversal
+ : symbol.union extends KS[number] ? Prism
+ : symbol.disjoint extends KS[number] ? Prism
+ : Lens
+
+export interface Traversal {
+ get(data: S): T
+ set(focus: A, data: S): S
+ set(focus: A): (data: S) => S
+ modify(fn: (focus: A) => B, source: S): Modify
+ modify(fn: (focus: A) => B): (source: S) => Modify
+ type: Profunctor.Optic.Type
+}
+
+export interface Lens {
get(data: S): A
set(focus: A, data: S): S
set(focus: A): (data: S) => S
@@ -650,6 +659,16 @@ export interface SchemaLens {
type: Profunctor.Optic.Type
}
+export interface Prism {
+ get(data: S): A
+ set(focus: A, data: S): S
+ set(focus: A): (data: S) => S
+ modify(fn: (focus: A) => B, source: S): Modify
+ modify(fn: (focus: A) => B): (source: S) => Modify
+ type: Profunctor.Optic.Type
+}
+
+
const areAllObjects = (x: unknown[]): x is Z.Object[] => x.every(tagged('object'))
function isDisjointUnionSchema(CURSOR: Z.Union) {
@@ -974,13 +993,13 @@ export function parsePath_(type: T, ...path: (keyof any)[])
export function makeLens<
Type extends z.ZodType,
- Proxy extends Proxy.new,
+ $ extends Proxy.new,
Target,
>(
type: Type,
- selector: (proxy: Proxy) => Target
+ selector: ($: $) => Target
// @ts-ignore
-): SchemaLens, Z.infer, Target[symbol.path]>
+): MakeLens, Target[symbol.type], Target[symbol.path]>
export function makeLens(type: Type, selector: Witness) {
const { proxy, revoke } = createProxy(type)
@@ -1002,29 +1021,48 @@ export declare namespace Proxy {
Proxy_object as object,
Proxy_optional as optional,
Proxy_primitive as primitive,
- Proxy_record as record,
+ RecordType as record,
Proxy_tuple as tuple,
Proxy_union as union,
}
}
+type Drop2 = S extends [any, any, ...infer T] ? T : never
-export type Omit_
- = never | [Exclude] extends [never]
- ? unknown
- : { [P in keyof T as P extends K ? never : P]: T[P] }
-
-/**
- * TODO:
- * Make this not break with arrays
- */
-export type Modify
- = KS extends [infer K extends keyof T] ? Omit_ & { [P in K]: V }
- : KS extends [infer K, ...infer Todo]
- ? K extends keyof T ? Omit_ & { [P in K]: Modify }
- : T
+export type Modify
+ = KS extends [infer K, ...infer Todo]
+ ? symbol.disjoint extends K ?
+ | Exclude
+ | (Drop2 extends infer Next ? Next extends [] ? V : Modify, V> : never)
+ : symbol.array extends K ? Modify
+ : symbol.record extends K ? Modify
+ : { [P in keyof T]: P extends K ? Modify : T[P] }
: T
+// export type Modify
+// = KS extends [] ? V
+// : KS extends [infer K, ...infer Todo]
+// ? symbol.disjoint extends K ?
+// | Exclude
+// | Modify, Extract, V>
+// : symbol.array extends K ? Modify
+// : { [P in keyof T]: P extends K ? Modify : T[P] }
+// : T
+
+// export type Modify
+// = KS extends [] ? V
+// : KS extends [infer K, ...infer Todo]
+// ? symbol.disjoint extends K ?
+// | Exclude
+// | Modify, Extract, V>
+// : { [P in keyof T]: P extends K ? Modify : T[P] }
+// : T
+
+// export type Modify
+// = KS extends [infer K] ? { [P in keyof T]: P extends K ? V : T[P] }
+// : KS extends [infer K, ...infer Todo]
+// ? { [P in keyof T]: P extends K ? Modify : T[P] }
+// : T
// function coalesceFallback(fallback: unknown, ...path: (keyof any)[]) {
// let Cmd: keyof any | undefined
diff --git a/packages/zod/test/lens.test.ts b/packages/zod/test/lens.test.ts
index 4970de9d..7f816e86 100644
--- a/packages/zod/test/lens.test.ts
+++ b/packages/zod/test/lens.test.ts
@@ -216,6 +216,26 @@ const BIG_SCHEMA = z.object({
vi.describe('γβ³οΈγβΉβΉβΉ β²@traversable/zodβ³: zx.makeLens', () => {
vi.it('temp 1', () => {
+ const schema_00 = z.object({ a: z.object({ b: z.number() }) })
+ const lens_00 = zx.makeLens(schema_00, $ => $.a.b)
+ const ex_00 = lens_00.modify($ => [$, $] satisfies [any, any], { a: { b: 1 } })
+ // ^?
+ vi.assertType<{ a: { b: [number, number] } }>(ex_00)
+
+ const schema_01 = z.object({ a: z.array(z.object({ b: z.number(), c: z.string() })) })
+ const lens_01 = zx.makeLens(schema_01, $ => $.a.α£κΈκΈ)
+ const ex_01 = lens_01.modify($ => [$.b, $.c] satisfies [any, any], { a: [] })
+ // ^?
+ vi.assertType<{ a: [number, string][] }>(ex_01)
+
+ const schema_02 = z.union([z.object({ tag: z.literal('ONE') }), z.object({ tag: z.literal('TWO') })])
+ const lens_02 = zx.makeLens(schema_02, $ => $.κONE)
+
+ // const ex_02 = lens_02.modify($ => ({ tag: [$.tag] }), { tag: 'ONE' })
+ const ex_02 = lens_02.modify($ => ({ tag: [$.tag] }), { tag: 'ONE' })
+ // ^?
+ vi.assertType<{ tag: 'ONE'[] } | { tag: 'TWO' }>(ex_02)
+
const LENS_040 = zx.makeLens(
BIG_SCHEMA,
(proxy) => proxy.A.Η.κ2.x