Skip to content

Commit

Permalink
Fix typing bug hiding errors of nullable composite fields (#1545)
Browse files Browse the repository at this point in the history
This commit changes types to match what the existing code produces.

For type: `z.object({ foo: z.string() })`
The error type was and still is `{ _errors: string[], foo?: { _errors: string[] } }`

But for type: `z.object({ foo: z.string() }).optional()`
It was previously `{ _errors: string[] }`
After this commit, it is `{ _errors: string[], foo?: { _errors: string[] } }`
  • Loading branch information
tadeokondrak committed Nov 14, 2022
1 parent 4671461 commit 9cc7535
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 12 deletions.
12 changes: 6 additions & 6 deletions deno/lib/ZodError.ts
Expand Up @@ -165,12 +165,12 @@ export const quotelessJson = (obj: any) => {

export type ZodFormattedError<T, U = string> = {
_errors: U[];
} & (T extends [any, ...any[]]
? { [K in keyof T]?: ZodFormattedError<T[K]> }
: T extends any[]
? { [k: number]: ZodFormattedError<T[number]> }
: T extends object
? { [K in keyof T]?: ZodFormattedError<T[K]> }
} & (NonNullable<T> extends [any, ...any[]]
? { [K in keyof NonNullable<T>]?: ZodFormattedError<NonNullable<T>[K]> }
: NonNullable<T> extends any[]
? { [k: number]: ZodFormattedError<NonNullable<T>[number]> }
: NonNullable<T> extends object
? { [K in keyof NonNullable<T>]?: ZodFormattedError<NonNullable<T>[K]> }
: unknown);

export type inferFormattedError<
Expand Down
41 changes: 41 additions & 0 deletions deno/lib/__tests__/error.test.ts
Expand Up @@ -316,6 +316,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<typeof schema>;
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:
Expand Down
12 changes: 6 additions & 6 deletions src/ZodError.ts
Expand Up @@ -165,12 +165,12 @@ export const quotelessJson = (obj: any) => {

export type ZodFormattedError<T, U = string> = {
_errors: U[];
} & (T extends [any, ...any[]]
? { [K in keyof T]?: ZodFormattedError<T[K]> }
: T extends any[]
? { [k: number]: ZodFormattedError<T[number]> }
: T extends object
? { [K in keyof T]?: ZodFormattedError<T[K]> }
} & (NonNullable<T> extends [any, ...any[]]
? { [K in keyof NonNullable<T>]?: ZodFormattedError<NonNullable<T>[K]> }
: NonNullable<T> extends any[]
? { [k: number]: ZodFormattedError<NonNullable<T>[number]> }
: NonNullable<T> extends object
? { [K in keyof NonNullable<T>]?: ZodFormattedError<NonNullable<T>[K]> }
: unknown);

export type inferFormattedError<
Expand Down
41 changes: 41 additions & 0 deletions src/__tests__/error.test.ts
Expand Up @@ -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<typeof schema>;
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:
Expand Down

0 comments on commit 9cc7535

Please sign in to comment.