Skip to content

Commit

Permalink
feat: add .catch error (#2087)
Browse files Browse the repository at this point in the history
* feat: add `.catch` error

* Update README.md

* Switch to ctx object, use getter, tweak readme

---------

Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
  • Loading branch information
0xWryth and colinhacks committed Feb 26, 2023
1 parent 7d40ba5 commit e559605
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 45 deletions.
13 changes: 8 additions & 5 deletions README.md
Expand Up @@ -2153,14 +2153,17 @@ numberWithCatch.parse(5); // => 5
numberWithCatch.parse("tuna"); // => 42
```

Optionally, you can pass a function into `.catch` that will be re-executed whenever a default value needs to be generated:
Optionally, you can pass a function into `.catch` that will be re-executed whenever a default value needs to be generated. A `ctx` object containing the caught error will be passed into this function.

```ts
const numberWithRandomCatch = z.number().catch(Math.random);
const numberWithRandomCatch = z.number().catch((ctx) => {
ctx.error; // the caught ZodError
return Math.random();
});

numberWithRandomDefault.parse("sup"); // => 0.4413456736055323
numberWithRandomDefault.parse("sup"); // => 0.1871840107401901
numberWithRandomDefault.parse("sup"); // => 0.7223408162401552
numberWithRandomCatch.parse("sup"); // => 0.4413456736055323
numberWithRandomCatch.parse("sup"); // => 0.1871840107401901
numberWithRandomCatch.parse("sup"); // => 0.7223408162401552
```

Conceptually, this is how Zod processes "catch values":
Expand Down
13 changes: 8 additions & 5 deletions deno/lib/README.md
Expand Up @@ -2153,14 +2153,17 @@ numberWithCatch.parse(5); // => 5
numberWithCatch.parse("tuna"); // => 42
```

Optionally, you can pass a function into `.catch` that will be re-executed whenever a default value needs to be generated:
Optionally, you can pass a function into `.catch` that will be re-executed whenever a default value needs to be generated. A `ctx` object containing the caught error will be passed into this function.

```ts
const numberWithRandomCatch = z.number().catch(Math.random);
const numberWithRandomCatch = z.number().catch((ctx) => {
ctx.error; // the caught ZodError
return Math.random();
});

numberWithRandomDefault.parse("sup"); // => 0.4413456736055323
numberWithRandomDefault.parse("sup"); // => 0.1871840107401901
numberWithRandomDefault.parse("sup"); // => 0.7223408162401552
numberWithRandomCatch.parse("sup"); // => 0.4413456736055323
numberWithRandomCatch.parse("sup"); // => 0.1871840107401901
numberWithRandomCatch.parse("sup"); // => 0.7223408162401552
```

Conceptually, this is how Zod processes "catch values":
Expand Down
30 changes: 30 additions & 0 deletions deno/lib/__tests__/catch.test.ts
Expand Up @@ -189,3 +189,33 @@ test("reported issues with nested usage", () => {
expect(issues[2].message).toMatch("boolean");
}
});

test("catch error", () => {
let catchError: z.ZodError | undefined = undefined;

const schema = z.object({
age: z.number(),
name: z.string().catch((ctx) => {
catchError = ctx.error;

return "John Doe";
}),
});

const result = schema.safeParse({
age: null,
name: null,
});

expect(result.success).toEqual(false);
expect(!result.success && result.error.issues.length).toEqual(1);
expect(!result.success && result.error.issues[0].message).toMatch("number");

expect(catchError).toBeInstanceOf(z.ZodError);
expect(
catchError !== undefined && (catchError as z.ZodError).issues.length
).toEqual(1);
expect(
catchError !== undefined && (catchError as z.ZodError).issues[0].message
).toMatch("string");
});
39 changes: 28 additions & 11 deletions deno/lib/types.ts
Expand Up @@ -447,7 +447,7 @@ export abstract class ZodType<
}

catch(def: Output): ZodCatch<this>;
catch(def: () => Output): ZodCatch<this>;
catch(def: (ctx: { error: ZodError }) => Output): ZodCatch<this>;
catch(def: any) {
const catchValueFunc = typeof def === "function" ? def : () => def;

Expand Down Expand Up @@ -4298,7 +4298,7 @@ export interface ZodCatchDef<
C extends T["_input"] = T["_input"]
> extends ZodTypeDef {
innerType: T;
catchValue: () => C;
catchValue: (ctx: { error: ZodError }) => C;
typeName: ZodFirstPartyTypeKind.ZodCatch;
}

Expand All @@ -4310,15 +4310,20 @@ export class ZodCatch<T extends ZodTypeAny> extends ZodType<
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
const { ctx } = this._processInputParams(input);

// newCtx is used to not collect issues from inner types in ctx
const newCtx: ParseContext = {
...ctx,
common: {
...ctx.common,
issues: [],
},
};

const result = this._def.innerType._parse({
data: ctx.data,
path: ctx.path,
data: newCtx.data,
path: newCtx.path,
parent: {
...ctx,
common: {
...ctx.common,
issues: [], // don't collect issues from inner type
},
...newCtx,
},
});

Expand All @@ -4327,14 +4332,26 @@ export class ZodCatch<T extends ZodTypeAny> extends ZodType<
return {
status: "valid",
value:
result.status === "valid" ? result.value : this._def.catchValue(),
result.status === "valid"
? result.value
: this._def.catchValue({
get error() {
return new ZodError(newCtx.common.issues);
},
}),
};
});
} else {
return {
status: "valid",
value:
result.status === "valid" ? result.value : this._def.catchValue(),
result.status === "valid"
? result.value
: this._def.catchValue({
get error() {
return new ZodError(newCtx.common.issues);
},
}),
};
}
}
Expand Down
19 changes: 6 additions & 13 deletions playground.ts
@@ -1,16 +1,9 @@
import { z } from "./src";

// const emoji = z.string().emoji();

const emojiRegex =
/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]|\uFE0E|\uFE0F)/;

function isEmoji(val: string) {
return [...val].every((char) => emojiRegex.test(char));
// benchmark
// run 10000 times
console.time("bench");
for (let i = 0; i < 10000; i++) {
z.string().catch("asdf").parse(5);
}
console.log(isEmoji("🍺👩‍🚀🫡"));
console.log(isEmoji("💚💙💜💛❤️"));
console.log(isEmoji(":-)"));
console.log(isEmoji("asdf"));
console.log(isEmoji("😀stuff"));
console.log(isEmoji("stuff😀"));
console.timeEnd("bench");
30 changes: 30 additions & 0 deletions src/__tests__/catch.test.ts
Expand Up @@ -188,3 +188,33 @@ test("reported issues with nested usage", () => {
expect(issues[2].message).toMatch("boolean");
}
});

test("catch error", () => {
let catchError: z.ZodError | undefined = undefined;

const schema = z.object({
age: z.number(),
name: z.string().catch((ctx) => {
catchError = ctx.error;

return "John Doe";
}),
});

const result = schema.safeParse({
age: null,
name: null,
});

expect(result.success).toEqual(false);
expect(!result.success && result.error.issues.length).toEqual(1);
expect(!result.success && result.error.issues[0].message).toMatch("number");

expect(catchError).toBeInstanceOf(z.ZodError);
expect(
catchError !== undefined && (catchError as z.ZodError).issues.length
).toEqual(1);
expect(
catchError !== undefined && (catchError as z.ZodError).issues[0].message
).toMatch("string");
});
39 changes: 28 additions & 11 deletions src/types.ts
Expand Up @@ -447,7 +447,7 @@ export abstract class ZodType<
}

catch(def: Output): ZodCatch<this>;
catch(def: () => Output): ZodCatch<this>;
catch(def: (ctx: { error: ZodError }) => Output): ZodCatch<this>;
catch(def: any) {
const catchValueFunc = typeof def === "function" ? def : () => def;

Expand Down Expand Up @@ -4298,7 +4298,7 @@ export interface ZodCatchDef<
C extends T["_input"] = T["_input"]
> extends ZodTypeDef {
innerType: T;
catchValue: () => C;
catchValue: (ctx: { error: ZodError }) => C;
typeName: ZodFirstPartyTypeKind.ZodCatch;
}

Expand All @@ -4310,15 +4310,20 @@ export class ZodCatch<T extends ZodTypeAny> extends ZodType<
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
const { ctx } = this._processInputParams(input);

// newCtx is used to not collect issues from inner types in ctx
const newCtx: ParseContext = {
...ctx,
common: {
...ctx.common,
issues: [],
},
};

const result = this._def.innerType._parse({
data: ctx.data,
path: ctx.path,
data: newCtx.data,
path: newCtx.path,
parent: {
...ctx,
common: {
...ctx.common,
issues: [], // don't collect issues from inner type
},
...newCtx,
},
});

Expand All @@ -4327,14 +4332,26 @@ export class ZodCatch<T extends ZodTypeAny> extends ZodType<
return {
status: "valid",
value:
result.status === "valid" ? result.value : this._def.catchValue(),
result.status === "valid"
? result.value
: this._def.catchValue({
get error() {
return new ZodError(newCtx.common.issues);
},
}),
};
});
} else {
return {
status: "valid",
value:
result.status === "valid" ? result.value : this._def.catchValue(),
result.status === "valid"
? result.value
: this._def.catchValue({
get error() {
return new ZodError(newCtx.common.issues);
},
}),
};
}
}
Expand Down

0 comments on commit e559605

Please sign in to comment.