From 95cac36818e6bba8354afb6e989610f4c26897e3 Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Wed, 16 Aug 2023 23:08:25 +1000 Subject: [PATCH 1/4] Create an example to test intersections and unions --- examples/App.tsx | 6 ++++ examples/intersection/index.tsx | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 examples/intersection/index.tsx diff --git a/examples/App.tsx b/examples/App.tsx index 4760c3f..e965eaa 100644 --- a/examples/App.tsx +++ b/examples/App.tsx @@ -8,6 +8,7 @@ import Nested from './nested' import Coerced from './coerced' import Conditional from './conditional' import Controlled from './controlled' +import Intersection from './intersection' import KitchenSink from './kitchen-sink' interface Example { @@ -47,6 +48,11 @@ const EXAMPLES: Example[] = [ path: '/controlled', component: Controlled, }, + { + name: 'Intersection Schema', + path: '/intersection', + component: Intersection, + }, { name: 'Kitchen Sink', path: '/kitchen-sink', diff --git a/examples/intersection/index.tsx b/examples/intersection/index.tsx new file mode 100644 index 0000000..a5a5088 --- /dev/null +++ b/examples/intersection/index.tsx @@ -0,0 +1,54 @@ +import { Errors, SubmitHandler, getValue, useForm } from '@stevent-team/react-zoom-form' +import { z } from 'zod' +import Output from '../Output' + +export const schema = z.intersection( + z.object({ + common: z.string(), + }), + z.discriminatedUnion('size', [ + z.object({ + size: z.literal('small'), + smallProperty: z.string(), + }), + z.object({ + size: z.literal('large'), + largeProperty: z.string(), + }), + ]), +) + +const Intersection = ({ onSubmit }: { onSubmit: SubmitHandler }) => { + const { fields, handleSubmit, isDirty } = useForm({ schema, initialValues: { size: 'small' } }) + + return <> +
+ + + + + + + + + {getValue(fields.size) === 'small' ? <> + + + + : <> + + + + } + + + + + + +} + +export default Intersection From 51c55b420d306d6fee6d78fdfe1517ef70c4976b Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Wed, 16 Aug 2023 23:11:00 +1000 Subject: [PATCH 2/4] Support intersections and unions in the field chain logic --- lib/utils.ts | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index 50c7ec7..ab697ce 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -50,11 +50,25 @@ export type PartialObject = T extends any[] ? T : Partial /** Excludes undefined from a type, but keeps null */ export type NonUndefined = T extends undefined ? never : T +const getZodObjectShape = (type: z.ZodType) => { + const unwrapped = unwrapZodType(type) + if (unwrapped instanceof z.ZodObject) return unwrapped.shape + return {} +} + export const unwrapZodType = (type: z.ZodType): z.ZodType => { if (type instanceof z.ZodObject || type instanceof z.ZodArray) return type if (type instanceof z.ZodEffects) return unwrapZodType(type.innerType()) + if ((type instanceof z.ZodDiscriminatedUnion || type instanceof z.ZodUnion) && Array.isArray(type.options)) { + return z.ZodObject.create(type.options.reduce((a, o) => ({ ...a, ...getZodObjectShape(o) }), {})) + } + + if (type instanceof z.ZodIntersection) { + return z.ZodObject.create({ ...getZodObjectShape(type._def.left), ...getZodObjectShape(type._def.right) }) + } + const anyType = type as any if (anyType._def?.innerType) return unwrapZodType(anyType._def.innerType) @@ -108,17 +122,16 @@ export const fieldChain = ( return fieldChain(unwrapped._def.type, [...path, { key: Number(key), type: 'array' }], register, fieldRefs, controls) } - // If the current Zod schema is not an array or object, we must be at a leaf node - if (!(unwrapped instanceof z.ZodObject)) { - // Leaf node functions - if (key === 'register') return (options: RegisterOptions = {}) => register(path, schema, controls.setFormValue, fieldRefs, options) - if (key === 'name') return () => path.map(p => p.key).join('.') - - // Attempted to access a property that didn't exist - throw new Error(`Expected ZodObject at "${path.map(p => p.key).join('.')}" got ${schema.constructor.name}`) + if (unwrapped instanceof z.ZodObject) { + return fieldChain(unwrapped.shape[key], [...path, { key, type: 'object' }], register, fieldRefs, controls) } - return fieldChain(unwrapped.shape[key], [...path, { key, type: 'object' }], register, fieldRefs, controls) + // Leaf node functions + if (key === 'register') return (options: RegisterOptions = {}) => register(path, schema, controls.setFormValue, fieldRefs, options) + if (key === 'name') return () => path.map(p => p.key).join('.') + + // Attempted to access a property that didn't exist + throw new Error(`Unsupported type at "${path.map(p => p.key).join('.')}" got ${schema.constructor.name}`) } }) as unknown // Never let them know your next move... From 3915be6736ec56e89348d01f2c452e89c4f309d8 Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Thu, 17 Aug 2023 13:47:38 +1000 Subject: [PATCH 3/4] Basic type support for intersections and unions --- lib/utils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/utils.ts b/lib/utils.ts index ab697ce..b8c23d8 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -4,12 +4,19 @@ import { z } from 'zod' // A Zod object that can hold nested data type AnyZodContainer = z.AnyZodObject | z.AnyZodTuple | z.ZodArray | z.ZodRecord | z.ZodMap | z.ZodSet +// Intersect everything inside an array after running each through the FieldChain type +type FieldChainEach = ArraySchema extends [infer First extends z.ZodType, ...infer Rest extends z.ZodType[]] + ? FieldChain & FieldChainEach + : unknown + // Creates the type for the field chain by recusively travelling through the Zod schema type RecursiveFieldChain = z.infer extends LeafValue ? z.infer : Schema extends z.AnyZodTuple ? { [K in keyof z.infer]: FieldChain } : Schema extends z.ZodArray ? { [k: number]: FieldChain } : Schema extends z.AnyZodObject ? { [K in keyof z.infer]: FieldChain } + : Schema extends z.ZodIntersection ? FieldChain & FieldChain + : Schema extends (z.ZodUnion | z.ZodDiscriminatedUnion) ? FieldChainEach : Schema extends (z.ZodDefault | z.ZodOptional | z.ZodNullable) ? FieldChain : Schema extends z.ZodEffects ? FieldChain : Schema extends z.ZodLazy ? FieldChain> From 72d8a731c892a7a3b8b3532557bea282bb450e26 Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Thu, 17 Aug 2023 13:48:22 +1000 Subject: [PATCH 4/4] Add changeset --- .changeset/perfect-peaches-design.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-peaches-design.md diff --git a/.changeset/perfect-peaches-design.md b/.changeset/perfect-peaches-design.md new file mode 100644 index 0000000..78eb976 --- /dev/null +++ b/.changeset/perfect-peaches-design.md @@ -0,0 +1,5 @@ +--- +"@stevent-team/react-zoom-form": minor +--- + +Support for intersections and unions in the Zod schema