diff --git a/deno/lib/README.md b/deno/lib/README.md index 6da6c688c..ddea4a19b 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -779,7 +779,7 @@ nullableString.parse(null); // => null Or use the `.nullable()` method. ```ts -const E = z.string().nullable(); // equivalent to D +const E = z.string().nullable(); // equivalent to nullableString type E = z.infer; // string | null ``` @@ -1967,7 +1967,7 @@ petCat(fido); // works fine In some cases, its can be desirable to simulate _nominal typing_ inside TypeScript. For instance, you may wish to write a function that only accepts an input that has been validated by Zod. This can be achieved with _branded types_ (AKA _opaque types_). ```ts -const Cat = z.object({ name: z.string }).brand<"Cat">(); +const Cat = z.object({ name: z.string() }).brand<"Cat">(); type Cat = z.infer; const petCat = (cat: Cat) => {}; @@ -1983,7 +1983,7 @@ petCat({ name: "fido" }); Under the hood, this works by attaching a "brand" to the inferred type using an intersection type. This way, plain/unbranded data structures are no longer assignable to the inferred type of the schema. ```ts -const Cat = z.object({ name: z.string }).brand<"Cat">(); +const Cat = z.object({ name: z.string() }).brand<"Cat">(); type Cat = z.infer; // {name: string} & {[symbol]: "Cat"} ``` diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index 26075791f..fd2d5adee 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -156,12 +156,12 @@ export const quotelessJson = (obj: any) => { export type ZodFormattedError = { _errors: U[]; -} & (T extends [any, ...any[]] - ? { [K in keyof T]?: ZodFormattedError } - : T extends any[] - ? { [k: number]: ZodFormattedError } - : T extends object - ? { [K in keyof T]?: ZodFormattedError } +} & (NonNullable extends [any, ...any[]] + ? { [K in keyof NonNullable]?: ZodFormattedError[K]> } + : NonNullable extends any[] + ? { [k: number]: ZodFormattedError[number]> } + : NonNullable extends object + ? { [K in keyof NonNullable]?: ZodFormattedError[K]> } : unknown); export type inferFormattedError< diff --git a/deno/lib/__tests__/error.test.ts b/deno/lib/__tests__/error.test.ts index d5437e802..5c36ba901 100644 --- a/deno/lib/__tests__/error.test.ts +++ b/deno/lib/__tests__/error.test.ts @@ -315,6 +315,47 @@ test("formatting", () => { } }); +test("formatting with nullable and optional fields", () => { + const nameSchema = z.string().refine((val) => val.length > 5); + const schema = z.object({ + nullableObject: z.object({ name: nameSchema }).nullable(), + nullableArray: z.array(nameSchema).nullable(), + nullableTuple: z.tuple([nameSchema, nameSchema, z.number()]).nullable(), + optionalObject: z.object({ name: nameSchema }).optional(), + optionalArray: z.array(nameSchema).optional(), + optionalTuple: z.tuple([nameSchema, nameSchema, z.number()]).optional(), + }); + const invalidItem = { + nullableObject: { name: "abcd" }, + nullableArray: ["abcd"], + nullableTuple: ["abcd", "abcd", 1], + optionalObject: { name: "abcd" }, + optionalArray: ["abcd"], + optionalTuple: ["abcd", "abcd", 1], + }; + const result = schema.safeParse(invalidItem); + expect(result.success).toEqual(false); + if (!result.success) { + type FormattedError = z.inferFormattedError; + const error: FormattedError = result.error.format(); + expect(error._errors).toEqual([]); + expect(error.nullableObject?._errors).toEqual([]); + expect(error.nullableObject?.name?._errors).toEqual(["Invalid input"]); + expect(error.nullableArray?._errors).toEqual([]); + expect(error.nullableArray?.[0]?._errors).toEqual(["Invalid input"]); + expect(error.nullableTuple?._errors).toEqual([]); + expect(error.nullableTuple?.[0]?._errors).toEqual(["Invalid input"]); + expect(error.nullableTuple?.[1]?._errors).toEqual(["Invalid input"]); + expect(error.optionalObject?._errors).toEqual([]); + expect(error.optionalObject?.name?._errors).toEqual(["Invalid input"]); + expect(error.optionalArray?._errors).toEqual([]); + expect(error.optionalArray?.[0]?._errors).toEqual(["Invalid input"]); + expect(error.optionalTuple?._errors).toEqual([]); + expect(error.optionalTuple?.[0]?._errors).toEqual(["Invalid input"]); + expect(error.optionalTuple?.[1]?._errors).toEqual(["Invalid input"]); + } +}) + const stringWithCustomError = z.string({ errorMap: (issue, ctx) => ({ message: diff --git a/src/ZodError.ts b/src/ZodError.ts index e1cc51c60..7033460c8 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -156,12 +156,12 @@ export const quotelessJson = (obj: any) => { export type ZodFormattedError = { _errors: U[]; -} & (T extends [any, ...any[]] - ? { [K in keyof T]?: ZodFormattedError } - : T extends any[] - ? { [k: number]: ZodFormattedError } - : T extends object - ? { [K in keyof T]?: ZodFormattedError } +} & (NonNullable extends [any, ...any[]] + ? { [K in keyof NonNullable]?: ZodFormattedError[K]> } + : NonNullable extends any[] + ? { [k: number]: ZodFormattedError[number]> } + : NonNullable extends object + ? { [K in keyof NonNullable]?: ZodFormattedError[K]> } : unknown); export type inferFormattedError< diff --git a/src/__tests__/error.test.ts b/src/__tests__/error.test.ts index 3834c8324..adfb09df1 100644 --- a/src/__tests__/error.test.ts +++ b/src/__tests__/error.test.ts @@ -314,6 +314,47 @@ test("formatting", () => { } }); +test("formatting with nullable and optional fields", () => { + const nameSchema = z.string().refine((val) => val.length > 5); + const schema = z.object({ + nullableObject: z.object({ name: nameSchema }).nullable(), + nullableArray: z.array(nameSchema).nullable(), + nullableTuple: z.tuple([nameSchema, nameSchema, z.number()]).nullable(), + optionalObject: z.object({ name: nameSchema }).optional(), + optionalArray: z.array(nameSchema).optional(), + optionalTuple: z.tuple([nameSchema, nameSchema, z.number()]).optional(), + }); + const invalidItem = { + nullableObject: { name: "abcd" }, + nullableArray: ["abcd"], + nullableTuple: ["abcd", "abcd", 1], + optionalObject: { name: "abcd" }, + optionalArray: ["abcd"], + optionalTuple: ["abcd", "abcd", 1], + }; + const result = schema.safeParse(invalidItem); + expect(result.success).toEqual(false); + if (!result.success) { + type FormattedError = z.inferFormattedError; + const error: FormattedError = result.error.format(); + expect(error._errors).toEqual([]); + expect(error.nullableObject?._errors).toEqual([]); + expect(error.nullableObject?.name?._errors).toEqual(["Invalid input"]); + expect(error.nullableArray?._errors).toEqual([]); + expect(error.nullableArray?.[0]?._errors).toEqual(["Invalid input"]); + expect(error.nullableTuple?._errors).toEqual([]); + expect(error.nullableTuple?.[0]?._errors).toEqual(["Invalid input"]); + expect(error.nullableTuple?.[1]?._errors).toEqual(["Invalid input"]); + expect(error.optionalObject?._errors).toEqual([]); + expect(error.optionalObject?.name?._errors).toEqual(["Invalid input"]); + expect(error.optionalArray?._errors).toEqual([]); + expect(error.optionalArray?.[0]?._errors).toEqual(["Invalid input"]); + expect(error.optionalTuple?._errors).toEqual([]); + expect(error.optionalTuple?.[0]?._errors).toEqual(["Invalid input"]); + expect(error.optionalTuple?.[1]?._errors).toEqual(["Invalid input"]); + } +}) + const stringWithCustomError = z.string({ errorMap: (issue, ctx) => ({ message: