Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix typing bug hiding errors of nullable composite fields #1545

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions deno/lib/README.md
Expand Up @@ -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<typeof E>; // string | null
```

Expand Down Expand Up @@ -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<typeof Cat>;

const petCat = (cat: Cat) => {};
Expand All @@ -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<typeof Cat>;
// {name: string} & {[symbol]: "Cat"}
```
Expand Down
12 changes: 6 additions & 6 deletions deno/lib/ZodError.ts
Expand Up @@ -156,12 +156,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 @@ -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
12 changes: 6 additions & 6 deletions src/ZodError.ts
Expand Up @@ -156,12 +156,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 @@ -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<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