Skip to content

Commit

Permalink
refactor: remove usage of instanceof for Zod schemas (#601)
Browse files Browse the repository at this point in the history
  • Loading branch information
colinhacks committed Apr 25, 2024
1 parent e1c28cb commit b4d4817
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 118 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ node_modules/
/test-results/
/playwright-report/
/playwright/.cache/

# DS Store
.DS_Store
**/.DS_Store
126 changes: 61 additions & 65 deletions packages/conform-zod/coercion.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import {
type ZodType,
type ZodTypeAny,
type output,
ZodString,
ZodEnum,
ZodLiteral,
ZodNumber,
ZodBoolean,
ZodDate,
ZodArray,
ZodBigInt,
ZodNativeEnum,
ZodObject,
ZodLazy,
ZodIntersection,
ZodUnion,
ZodDiscriminatedUnion,
ZodTuple,
ZodPipeline,
ZodEffects,
ZodAny,
ZodNullable,
ZodOptional,
ZodDefault,
lazy,
any,
ZodCatch,
} from 'zod';
import type {
ZodDiscriminatedUnionOption,
ZodFirstPartySchemaTypes,
ZodType,
ZodTypeAny,
output,
} from 'zod';

/**
* Helpers for coercing string value
Expand Down Expand Up @@ -80,7 +74,7 @@ export function isFileSchema(schema: ZodEffects<any, any, any>): boolean {

return (
schema._def.effect.type === 'refinement' &&
schema.innerType() instanceof ZodAny &&
schema.innerType()._def.typeName === 'ZodAny' &&
schema.safeParse(new File([], '')).success &&
!schema.safeParse('').success
);
Expand Down Expand Up @@ -110,31 +104,32 @@ export function enableTypeCoercion<Schema extends ZodTypeAny>(
}

let schema: ZodTypeAny = type;
let def = (type as ZodFirstPartySchemaTypes)._def;

if (
type instanceof ZodString ||
type instanceof ZodLiteral ||
type instanceof ZodEnum ||
type instanceof ZodNativeEnum
def.typeName === 'ZodString' ||
def.typeName === 'ZodLiteral' ||
def.typeName === 'ZodEnum' ||
def.typeName === 'ZodNativeEnum'
) {
schema = any()
.transform((value) => coerceString(value))
.pipe(type);
} else if (type instanceof ZodNumber) {
} else if (def.typeName === 'ZodNumber') {
schema = any()
.transform((value) =>
coerceString(value, (text) =>
text.trim() === '' ? Number.NaN : Number(text),
),
)
.pipe(type);
} else if (type instanceof ZodBoolean) {
} else if (def.typeName === 'ZodBoolean') {
schema = any()
.transform((value) =>
coerceString(value, (text) => (text === 'on' ? true : text)),
)
.pipe(type);
} else if (type instanceof ZodDate) {
} else if (def.typeName === 'ZodDate') {
schema = any()
.transform((value) =>
coerceString(value, (timestamp) => {
Expand All @@ -151,11 +146,11 @@ export function enableTypeCoercion<Schema extends ZodTypeAny>(
}),
)
.pipe(type);
} else if (type instanceof ZodBigInt) {
} else if (def.typeName === 'ZodBigInt') {
schema = any()
.transform((value) => coerceString(value, BigInt))
.pipe(type);
} else if (type instanceof ZodArray) {
} else if (def.typeName === 'ZodArray') {
schema = any()
.transform((value) => {
// No preprocess needed if the value is already an array
Expand All @@ -175,102 +170,103 @@ export function enableTypeCoercion<Schema extends ZodTypeAny>(
})
.pipe(
new ZodArray({
...type._def,
type: enableTypeCoercion(type.element, cache),
...def,
type: enableTypeCoercion(def.type, cache),
}),
);
} else if (type instanceof ZodObject) {
} else if (def.typeName === 'ZodObject') {
const shape = Object.fromEntries(
Object.entries(type.shape).map(([key, def]) => [
Object.entries(def.shape()).map(([key, def]) => [
key,
// @ts-expect-error see message above
enableTypeCoercion(def, cache),
]),
);
schema = new ZodObject({
...type._def,
...def,
shape: () => shape,
});
} else if (type instanceof ZodEffects) {
if (isFileSchema(type)) {
} else if (def.typeName === 'ZodEffects') {
if (isFileSchema(type as unknown as ZodEffects<any, any, any>)) {
schema = any()
.transform((value) => coerceFile(value))
.pipe(type);
} else {
schema = new ZodEffects({
...type._def,
schema: enableTypeCoercion(type.innerType(), cache),
...def,
schema: enableTypeCoercion(def.schema, cache),
});
}
} else if (type instanceof ZodOptional) {
} else if (def.typeName === 'ZodOptional') {
schema = any()
.transform((value) => coerceFile(coerceString(value)))
.pipe(
new ZodOptional({
...type._def,
innerType: enableTypeCoercion(type.unwrap(), cache),
...def,
innerType: enableTypeCoercion(def.innerType, cache),
}),
);
} else if (type instanceof ZodDefault) {
} else if (def.typeName === 'ZodDefault') {
schema = any()
.transform((value) => coerceFile(coerceString(value)))
.pipe(
new ZodDefault({
...type._def,
innerType: enableTypeCoercion(type.removeDefault(), cache),
...def,
innerType: enableTypeCoercion(def.innerType, cache),
}),
);
} else if (type instanceof ZodCatch) {
} else if (def.typeName === 'ZodCatch') {
schema = new ZodCatch({
...type._def,
innerType: enableTypeCoercion(type.removeCatch(), cache),
...def,
innerType: enableTypeCoercion(def.innerType, cache),
});
} else if (type instanceof ZodIntersection) {
} else if (def.typeName === 'ZodIntersection') {
schema = new ZodIntersection({
...type._def,
left: enableTypeCoercion(type._def.left, cache),
right: enableTypeCoercion(type._def.right, cache),
...def,
left: enableTypeCoercion(def.left, cache),
right: enableTypeCoercion(def.right, cache),
});
} else if (type instanceof ZodUnion) {
} else if (def.typeName === 'ZodUnion') {
schema = new ZodUnion({
...type._def,
options: type.options.map((option: ZodTypeAny) =>
...def,
options: def.options.map((option: ZodTypeAny) =>
enableTypeCoercion(option, cache),
),
});
} else if (type instanceof ZodDiscriminatedUnion) {
} else if (def.typeName === 'ZodDiscriminatedUnion') {
schema = new ZodDiscriminatedUnion({
...type._def,
options: type.options.map((option: ZodTypeAny) =>
...def,
options: def.options.map((option: ZodTypeAny) =>
enableTypeCoercion(option, cache),
),
optionsMap: new Map(
Array.from(type.optionsMap.entries()).map(([discriminator, option]) => [
Array.from(def.optionsMap.entries()).map(([discriminator, option]) => [
discriminator,
enableTypeCoercion(option, cache),
enableTypeCoercion(option, cache) as ZodDiscriminatedUnionOption<any>,
]),
),
});
} else if (type instanceof ZodTuple) {
} else if (def.typeName === 'ZodTuple') {
schema = new ZodTuple({
...type._def,
items: type.items.map((item: ZodTypeAny) =>
...def,
items: def.items.map((item: ZodTypeAny) =>
enableTypeCoercion(item, cache),
),
});
} else if (type instanceof ZodNullable) {
} else if (def.typeName === 'ZodNullable') {
schema = new ZodNullable({
...type._def,
innerType: enableTypeCoercion(type.unwrap(), cache),
...def,
innerType: enableTypeCoercion(def.innerType, cache),
});
} else if (type instanceof ZodPipeline) {
} else if (def.typeName === 'ZodPipeline') {
schema = new ZodPipeline({
...type._def,
in: enableTypeCoercion(type._def.in, cache),
out: enableTypeCoercion(type._def.out, cache),
...def,
in: enableTypeCoercion(def.in, cache),
out: enableTypeCoercion(def.out, cache),
});
} else if (type instanceof ZodLazy) {
schema = lazy(() => enableTypeCoercion(type.schema, cache));
} else if (def.typeName === 'ZodLazy') {
const inner = def.getter();
schema = lazy(() => enableTypeCoercion(inner, cache));
}

if (type !== schema) {
Expand Down
95 changes: 42 additions & 53 deletions packages/conform-zod/constraint.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import type { Constraint } from '@conform-to/dom';
import {
type ZodTypeAny,
ZodArray,
ZodDefault,
ZodDiscriminatedUnion,
ZodEffects,
ZodEnum,
ZodIntersection,

import type {
ZodTypeAny,
ZodFirstPartySchemaTypes,
ZodNumber,
ZodObject,
ZodOptional,
ZodPipeline,
ZodString,
ZodUnion,
ZodTuple,
ZodLazy,
} from 'zod';

const keys: Array<keyof Constraint> = [
Expand All @@ -37,35 +27,32 @@ export function getZodConstraint(
name = '',
): void {
const constraint = name !== '' ? (data[name] ??= { required: true }) : {};
const def = (schema as ZodFirstPartySchemaTypes)['_def'];

if (schema instanceof ZodObject) {
for (const key in schema.shape) {
updateConstraint(
schema.shape[key],
data,
name ? `${name}.${key}` : key,
);
if (def.typeName === 'ZodObject') {
for (const key in def.shape()) {
updateConstraint(def.shape()[key], data, name ? `${name}.${key}` : key);
}
} else if (schema instanceof ZodEffects) {
updateConstraint(schema.innerType(), data, name);
} else if (schema instanceof ZodPipeline) {
} else if (def.typeName === 'ZodEffects') {
updateConstraint(def.schema, data, name);
} else if (def.typeName === 'ZodPipeline') {
// FIXME: What to do with .pipe()?
updateConstraint(schema._def.out, data, name);
} else if (schema instanceof ZodIntersection) {
updateConstraint(def.out, data, name);
} else if (def.typeName === 'ZodIntersection') {
const leftResult: Record<string, Constraint> = {};
const rightResult: Record<string, Constraint> = {};

updateConstraint(schema._def.left, leftResult, name);
updateConstraint(schema._def.right, rightResult, name);
updateConstraint(def.left, leftResult, name);
updateConstraint(def.right, rightResult, name);

Object.assign(data, leftResult, rightResult);
} else if (
schema instanceof ZodUnion ||
schema instanceof ZodDiscriminatedUnion
def.typeName === 'ZodUnion' ||
def.typeName === 'ZodDiscriminatedUnion'
) {
Object.assign(
data,
(schema.options as ZodTypeAny[])
(def.options as ZodTypeAny[])
.map((option) => {
const result: Record<string, Constraint> = {};

Expand Down Expand Up @@ -111,41 +98,43 @@ export function getZodConstraint(
} else if (name === '') {
// All the cases below are not allowed on root
throw new Error('Unsupported schema');
} else if (schema instanceof ZodArray) {
} else if (def.typeName === 'ZodArray') {
constraint.multiple = true;
updateConstraint(schema.element, data, `${name}[]`);
} else if (schema instanceof ZodString) {
if (schema.minLength !== null) {
constraint.minLength = schema.minLength;
updateConstraint(def.type, data, `${name}[]`);
} else if (def.typeName === 'ZodString') {
let _schema = schema as ZodString;
if (_schema.minLength !== null) {
constraint.minLength = _schema.minLength ?? undefined;
}
if (schema.maxLength !== null) {
constraint.maxLength = schema.maxLength;
if (_schema.maxLength !== null) {
constraint.maxLength = _schema.maxLength;
}
} else if (schema instanceof ZodOptional) {
} else if (def.typeName === 'ZodOptional') {
constraint.required = false;
updateConstraint(schema.unwrap(), data, name);
} else if (schema instanceof ZodDefault) {
updateConstraint(def.innerType, data, name);
} else if (def.typeName === 'ZodDefault') {
constraint.required = false;
updateConstraint(schema.removeDefault(), data, name);
} else if (schema instanceof ZodNumber) {
if (schema.minValue !== null) {
constraint.min = schema.minValue;
updateConstraint(def.innerType, data, name);
} else if (def.typeName === 'ZodNumber') {
let _schema = schema as ZodNumber;
if (_schema.minValue !== null) {
constraint.min = _schema.minValue;
}
if (schema.maxValue !== null) {
constraint.max = schema.maxValue;
if (_schema.maxValue !== null) {
constraint.max = _schema.maxValue;
}
} else if (schema instanceof ZodEnum) {
constraint.pattern = schema.options
} else if (def.typeName === 'ZodEnum') {
constraint.pattern = def.values
.map((option: string) =>
// To escape unsafe characters on regex
option.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d'),
)
.join('|');
} else if (schema instanceof ZodTuple) {
for (let i = 0; i < schema.items.length; i++) {
updateConstraint(schema.items[i], data, `${name}[${i}]`);
} else if (def.typeName === 'ZodTuple') {
for (let i = 0; i < def.items.length; i++) {
updateConstraint(def.items[i], data, `${name}[${i}]`);
}
} else if (schema instanceof ZodLazy) {
} else if (def.typeName === 'ZodLazy') {
// FIXME: If you are interested in this, feel free to create a PR
}
}
Expand Down

0 comments on commit b4d4817

Please sign in to comment.