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

add ZodNumber.safe() & ZodNumber.isSafe. #1753

Merged
merged 6 commits into from
Mar 4, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ z.number().nonpositive(); // <= 0
z.number().multipleOf(5); // Evenly divisible by 5. Alias .step(5)

z.number().finite(); // value must be finite, not Infinity or -Infinity
z.number().safe(); // value must be between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER
```

Optionally, you can pass in a second argument to provide a custom error message.
Expand Down
31 changes: 16 additions & 15 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@
- [Sponsors](#sponsors)
- [Ecosystem](#ecosystem)
- [Installation](#installation)
- [Node/npm](#nodenpm)
- [Deno](#deno)
- [Requirements](#requirements)
- [Node/npm](#from-npm-nodebun)
- [Deno](#from-denolandx-deno)
- [Basic usage](#basic-usage)
- [Primitives](#primitives)
- [Literals](#literals)
Expand Down Expand Up @@ -672,6 +673,7 @@ z.number().nonpositive(); // <= 0
z.number().multipleOf(5); // Evenly divisible by 5. Alias .step(5)

z.number().finite(); // value must be finite, not Infinity or -Infinity
z.number().safe(); // value must be between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER
```

Optionally, you can pass in a second argument to provide a custom error message.
Expand Down Expand Up @@ -1294,7 +1296,7 @@ const myUnion = z.discriminatedUnion("status", [
z.object({ status: z.literal("failed"), error: z.instanceof(Error) }),
]);

myUnion.parse({ type: "success", data: "yippie ki yay" });
myUnion.parse({ status: "success", data: "yippie ki yay" });
```

## Records
Expand Down Expand Up @@ -1682,7 +1684,7 @@ Given any Zod schema, you can call its `.parse` method to check `data` is valid.
const stringSchema = z.string();

stringSchema.parse("fish"); // => returns "fish"
stringSchema.parse(12); // throws Error('Non-string type: number');
stringSchema.parse(12); // throws error
```

### `.parseAsync`
Expand All @@ -1692,11 +1694,10 @@ stringSchema.parse(12); // throws Error('Non-string type: number');
If you use asynchronous [refinements](#refine) or [transforms](#transform) (more on those later), you'll need to use `.parseAsync`

```ts
const stringSchema1 = z.string().refine(async (val) => val.length < 20);
const value1 = await stringSchema.parseAsync("hello"); // => hello
const stringSchema = z.string().refine(async (val) => val.length <= 8);

const stringSchema2 = z.string().refine(async (val) => val.length > 20);
const value2 = await stringSchema.parseAsync("hello"); // => throws
await stringSchema.parseAsync("hello"); // => returns "hello"
await stringSchema.parseAsync("hello world"); // => throws error
```

### `.safeParse`
Expand Down Expand Up @@ -1781,7 +1782,7 @@ type RefineParams = {
};
```

For advanced cases, the second argument can also be a function that returns `RefineParams`/
For advanced cases, the second argument can also be a function that returns `RefineParams`.

```ts
const longString = z.string().refine(
Expand Down Expand Up @@ -1921,7 +1922,7 @@ const schema = z.number().superRefine((val, ctx) => {

#### Type refinements

If you provide a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) to `.refine()` or `superRefine()`, the resulting type will be narrowed down to your predicate's type. This is useful if you are mixing multiple chained refinements and transformations:
If you provide a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) to `.refine()` or `.superRefine()`, the resulting type will be narrowed down to your predicate's type. This is useful if you are mixing multiple chained refinements and transformations:

```ts
const schema = z
Expand All @@ -1936,15 +1937,15 @@ const schema = z
code: z.ZodIssueCode.custom, // customize your issue
message: "object should exist",
});
return false;
}
return true;

return z.NEVER; // The return value is not used, but we need to return something to satisfy the typing
})
// here, TS knows that arg is not null
.refine((arg) => arg.first === "bob", "`first` is not `bob`!");
```

> ⚠️ You must **still** call `ctx.addIssue()` if using `superRefine()` with a type predicate function. Otherwise the refinement won't be validated.
> ⚠️ You **must** use `ctx.addIssue()` instead of returning a boolean value to indicate whether the validation passes. If `ctx.addIssue` is _not_ called during the execution of the function, validation passes.

### `.transform`

Expand All @@ -1971,12 +1972,12 @@ emailToDomain.parse("[email protected]"); // => example.com

#### Validating during transform

The `.transform` method can simultaneously validate and transform the value. This is often simpler and less duplicative than chaining `refine` and `validate`.
The `.transform` method can simultaneously validate and transform the value. This is often simpler and less duplicative than chaining `transform` and `refine`.

As with `.superRefine`, the transform function receives a `ctx` object with a `addIssue` method that can be used to register validation issues.

```ts
const Strings = z.string().transform((val, ctx) => {
const numberInString = z.string().transform((val, ctx) => {
const parsed = parseInt(val);
if (isNaN(parsed)) {
ctx.addIssue({
Expand Down
9 changes: 9 additions & 0 deletions deno/lib/__tests__/number.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const nonnegative = z.number().nonnegative();
const multipleOfFive = z.number().multipleOf(5);
const multipleOfNegativeFive = z.number().multipleOf(-5);
const finite = z.number().finite();
const safe = z.number().safe();
const stepPointOne = z.number().step(0.1);
const stepPointZeroZeroZeroOne = z.number().step(0.0001);
const stepSixPointFour = z.number().step(6.4);
Expand Down Expand Up @@ -58,6 +59,8 @@ test("passing validations", () => {
multipleOfNegativeFive.parse(-15);
multipleOfNegativeFive.parse(15);
finite.parse(123);
safe.parse(Number.MIN_SAFE_INTEGER);
safe.parse(Number.MAX_SAFE_INTEGER);
stepPointOne.parse(6);
stepPointOne.parse(6.1);
stepPointOne.parse(6.1);
Expand Down Expand Up @@ -85,6 +88,8 @@ test("failing validations", () => {
expect(() => multipleOfNegativeFive.parse(7.5)).toThrow();
expect(() => finite.parse(Infinity)).toThrow();
expect(() => finite.parse(-Infinity)).toThrow();
expect(() => safe.parse(Number.MIN_SAFE_INTEGER - 1)).toThrow();
expect(() => safe.parse(Number.MAX_SAFE_INTEGER + 1)).toThrow();

expect(() => stepPointOne.parse(6.11)).toThrow();
expect(() => stepPointOne.parse(6.1000000001)).toThrow();
Expand All @@ -111,6 +116,7 @@ test("min max getters", () => {
expect(minFive.min(10).minValue).toEqual(10);
expect(positive.minValue).toEqual(0);
expect(nonnegative.minValue).toEqual(0);
expect(safe.minValue).toEqual(Number.MIN_SAFE_INTEGER);

expect(z.number().maxValue).toBeNull;
expect(gtFive.maxValue).toBeNull;
Expand All @@ -127,6 +133,7 @@ test("min max getters", () => {
expect(maxFive.max(1).maxValue).toEqual(1);
expect(negative.maxValue).toEqual(0);
expect(nonpositive.maxValue).toEqual(0);
expect(safe.maxValue).toEqual(Number.MAX_SAFE_INTEGER);
});

test("int getter", () => {
Expand All @@ -143,6 +150,7 @@ test("int getter", () => {
expect(maxFive.isInt).toEqual(false);
expect(negative.isInt).toEqual(false);
expect(nonpositive.isInt).toEqual(false);
expect(safe.isInt).toEqual(false);

expect(intNum.isInt).toEqual(true);
expect(multipleOfFive.isInt).toEqual(true);
Expand All @@ -165,4 +173,5 @@ test("finite getter", () => {
expect(intNum.isFinite).toEqual(true);
expect(multipleOfFive.isFinite).toEqual(true);
expect(z.number().min(5).max(10).isFinite).toEqual(true);
expect(safe.isFinite).toEqual(true);
});
34 changes: 25 additions & 9 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
message: errorUtil.toString(message),
});
}
step = this.multipleOf;

finite(message?: errorUtil.ErrMessage) {
return this._addCheck({
Expand All @@ -1128,7 +1129,19 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
});
}

step = this.multipleOf;
safe(message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "min",
inclusive: true,
value: Number.MIN_SAFE_INTEGER,
message: errorUtil.toString(message),
})._addCheck({
kind: "max",
inclusive: true,
value: Number.MAX_SAFE_INTEGER,
message: errorUtil.toString(message),
});
}

get minValue() {
let min: number | null = null;
Expand Down Expand Up @@ -1713,7 +1726,7 @@ export class ZodArray<

if (ctx.common.async) {
return Promise.all(
(ctx.data as any[]).map((item, i) => {
([...ctx.data] as any[]).map((item, i) => {
return def.type._parseAsync(
new ParseInputLazyPath(ctx, item, ctx.path, i)
);
Expand All @@ -1723,7 +1736,7 @@ export class ZodArray<
});
}

const result = (ctx.data as any[]).map((item, i) => {
const result = ([...ctx.data] as any[]).map((item, i) => {
return def.type._parseSync(
new ParseInputLazyPath(ctx, item, ctx.path, i)
);
Expand Down Expand Up @@ -3356,7 +3369,7 @@ export class ZodFunction<
return this._def.returns;
}

args<Items extends Parameters<typeof ZodTuple["create"]>[0]>(
args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>(
...items: Items
): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> {
return new ZodFunction({
Expand Down Expand Up @@ -4434,15 +4447,18 @@ const oboolean = () => booleanType().optional();

export const coerce = {
string: ((arg) =>
ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"],
ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"],
number: ((arg) =>
ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"],
ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"],
boolean: ((arg) =>
ZodBoolean.create({ ...arg, coerce: true })) as typeof ZodBoolean["create"],
ZodBoolean.create({
...arg,
coerce: true,
})) as (typeof ZodBoolean)["create"],
bigint: ((arg) =>
ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"],
ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"],
date: ((arg) =>
ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"],
ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"],
};

export {
Expand Down
9 changes: 9 additions & 0 deletions src/__tests__/number.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const nonnegative = z.number().nonnegative();
const multipleOfFive = z.number().multipleOf(5);
const multipleOfNegativeFive = z.number().multipleOf(-5);
const finite = z.number().finite();
const safe = z.number().safe();
const stepPointOne = z.number().step(0.1);
const stepPointZeroZeroZeroOne = z.number().step(0.0001);
const stepSixPointFour = z.number().step(6.4);
Expand Down Expand Up @@ -57,6 +58,8 @@ test("passing validations", () => {
multipleOfNegativeFive.parse(-15);
multipleOfNegativeFive.parse(15);
finite.parse(123);
safe.parse(Number.MIN_SAFE_INTEGER);
safe.parse(Number.MAX_SAFE_INTEGER);
stepPointOne.parse(6);
stepPointOne.parse(6.1);
stepPointOne.parse(6.1);
Expand Down Expand Up @@ -84,6 +87,8 @@ test("failing validations", () => {
expect(() => multipleOfNegativeFive.parse(7.5)).toThrow();
expect(() => finite.parse(Infinity)).toThrow();
expect(() => finite.parse(-Infinity)).toThrow();
expect(() => safe.parse(Number.MIN_SAFE_INTEGER - 1)).toThrow();
expect(() => safe.parse(Number.MAX_SAFE_INTEGER + 1)).toThrow();

expect(() => stepPointOne.parse(6.11)).toThrow();
expect(() => stepPointOne.parse(6.1000000001)).toThrow();
Expand All @@ -110,6 +115,7 @@ test("min max getters", () => {
expect(minFive.min(10).minValue).toEqual(10);
expect(positive.minValue).toEqual(0);
expect(nonnegative.minValue).toEqual(0);
expect(safe.minValue).toEqual(Number.MIN_SAFE_INTEGER);

expect(z.number().maxValue).toBeNull;
expect(gtFive.maxValue).toBeNull;
Expand All @@ -126,6 +132,7 @@ test("min max getters", () => {
expect(maxFive.max(1).maxValue).toEqual(1);
expect(negative.maxValue).toEqual(0);
expect(nonpositive.maxValue).toEqual(0);
expect(safe.maxValue).toEqual(Number.MAX_SAFE_INTEGER);
});

test("int getter", () => {
Expand All @@ -142,6 +149,7 @@ test("int getter", () => {
expect(maxFive.isInt).toEqual(false);
expect(negative.isInt).toEqual(false);
expect(nonpositive.isInt).toEqual(false);
expect(safe.isInt).toEqual(false);

expect(intNum.isInt).toEqual(true);
expect(multipleOfFive.isInt).toEqual(true);
Expand All @@ -164,4 +172,5 @@ test("finite getter", () => {
expect(intNum.isFinite).toEqual(true);
expect(multipleOfFive.isFinite).toEqual(true);
expect(z.number().min(5).max(10).isFinite).toEqual(true);
expect(safe.isFinite).toEqual(true);
});
30 changes: 23 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
message: errorUtil.toString(message),
});
}
step = this.multipleOf;

finite(message?: errorUtil.ErrMessage) {
return this._addCheck({
Expand All @@ -1128,7 +1129,19 @@ export class ZodNumber extends ZodType<number, ZodNumberDef> {
});
}

step = this.multipleOf;
safe(message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "min",
inclusive: true,
value: Number.MIN_SAFE_INTEGER,
message: errorUtil.toString(message),
})._addCheck({
kind: "max",
inclusive: true,
value: Number.MAX_SAFE_INTEGER,
message: errorUtil.toString(message),
});
}

get minValue() {
let min: number | null = null;
Expand Down Expand Up @@ -3356,7 +3369,7 @@ export class ZodFunction<
return this._def.returns;
}

args<Items extends Parameters<typeof ZodTuple["create"]>[0]>(
args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>(
...items: Items
): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> {
return new ZodFunction({
Expand Down Expand Up @@ -4434,15 +4447,18 @@ const oboolean = () => booleanType().optional();

export const coerce = {
string: ((arg) =>
ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"],
ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"],
number: ((arg) =>
ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"],
ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"],
boolean: ((arg) =>
ZodBoolean.create({ ...arg, coerce: true })) as typeof ZodBoolean["create"],
ZodBoolean.create({
...arg,
coerce: true,
})) as (typeof ZodBoolean)["create"],
bigint: ((arg) =>
ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"],
ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"],
date: ((arg) =>
ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"],
ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"],
};

export {
Expand Down