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

feat: added support for type negation with z.not() and .not() method … #3709

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
- [Dates](#dates)
- [Zod enums](#zod-enums)
- [Native enums](#native-enums)
- [Not](#not)
- [Optionals](#optionals)
- [Nullables](#nullables)
- [Objects](#objects)
Expand Down Expand Up @@ -1133,6 +1134,27 @@ You can access the underlying object with the `.enum` property:
FruitEnum.enum.Apple; // "apple"
```

## Not

You can use `z.not()` to create a schema that rejects a specific type. This wraps the schema in a `ZodNot` instance and returns the result.

```ts
const schema = z.not(z.string());

schema.parse(1234); // => passes, as 1234 is not a string
schema.parse("hello"); // => throws an error, as "hello" is a string
```

For convenience, you can also call the `.not()` method on an existing schema.

```ts
const schema = z.string().email().not();

schema.parse(1234); // => passes, as 1234 is not a string or a valid email
schema.parse("hello"); // => passes, as "hello" is not a valid email
schema.parse("[email protected]"); // => throws an error, as "[email protected]" is a valid email
```

## Optionals

You can make any schema optional with `z.optional()`. This wraps the schema in a `ZodOptional` instance and returns the result.
Expand Down
22 changes: 22 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
- [.min/.max/.length](#minmaxlength)
- [Unions](#unions)
- [Discriminated unions](#discriminated-unions)
- [Not](#not)
- [Optionals](#optionals)
- [Nullables](#nullables)
- [Enums](#enums)
Expand Down Expand Up @@ -941,6 +942,27 @@ const B = z.discriminatedUnion("status", [
const AB = z.discriminatedUnion("status", [...A.options, ...B.options]);
```

## Not

你可以用 `z.not()` 来创建一个拒绝特定类型的模式。这个方法会将模式包装在 `ZodNot` 实例中并返回结果。

```ts
const schema = z.not(z.string());

schema.parse(1234); // => 通过,因为 1234 不是字符串
schema.parse("hello"); // => 抛出错误,因为 "hello" 是字符串
```

为了方便,你也可以在现有的模式上直接调用 `.not()` 方法。

```ts
const schema = z.string().email().not();

schema.parse(1234); // => 通过,因为 1234 不是字符串或有效的邮箱地址
schema.parse("hello"); // => 通过,因为 "hello" 不是有效的邮箱地址
schema.parse("[email protected]"); // => 抛出错误,因为 "[email protected]" 是一个有效的邮箱地址
```

## Optionals

你可以用`z.optional()`使任何模式成为可选:
Expand Down
27 changes: 25 additions & 2 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
- [Dates](#dates)
- [Zod enums](#zod-enums)
- [Native enums](#native-enums)
- [Not](#not)
- [Optionals](#optionals)
- [Nullables](#nullables)
- [Objects](#objects)
Expand Down Expand Up @@ -252,7 +253,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
<td align="center">
<p></p>
<p>
<a href="https://speakeasyapi.dev/?utm_source=zod+docs">
<a href="https://speakeasy.com/?utm_source=zod+docs">
<picture height="40px">
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/colinhacks/zod/assets/3084745/b1d86601-c7fb-483c-9927-5dc24ce8b737">
<img alt="speakeasy" height="40px" src="https://github.com/colinhacks/zod/assets/3084745/647524a4-22bb-4199-be70-404207a5a2b5">
Expand All @@ -261,7 +262,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
<br />
SDKs & Terraform providers for your API
<br/>
<a href="https://speakeasyapi.dev/?utm_source=zod+docs" style="text-decoration:none;">speakeasyapi.dev</a>
<a href="https://speakeasy.com/?utm_source=zod+docs" style="text-decoration:none;">speakeasy.com</a>
</p>
<p></p>
</td>
Expand Down Expand Up @@ -490,6 +491,7 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`zod-prisma`](https://github.com/CarterGrimmeisen/zod-prisma): Generate Zod schemas from your Prisma schema.
- [`Supervillain`](https://github.com/Southclaws/supervillain): Generate Zod schemas from your Go structs.
- [`prisma-zod-generator`](https://github.com/omar-dulaimi/prisma-zod-generator): Emit Zod schemas from your Prisma schema.
- [`drizzle-zod`](https://orm.drizzle.team/docs/zod): Emit Zod schemas from your Drizzle schema.
- [`prisma-trpc-generator`](https://github.com/omar-dulaimi/prisma-trpc-generator): Emit fully implemented tRPC routers and their validation schemas using Zod.
- [`zod-prisma-types`](https://github.com/chrishoermann/zod-prisma-types) Create Zod types from your Prisma models.
- [`quicktype`](https://app.quicktype.io/): Convert JSON objects and JSON schemas into Zod schemas.
Expand Down Expand Up @@ -1132,6 +1134,27 @@ You can access the underlying object with the `.enum` property:
FruitEnum.enum.Apple; // "apple"
```

## Not

You can use `z.not()` to create a schema that rejects a specific type. This wraps the schema in a `ZodNot` instance and returns the result.

```ts
const schema = z.not(z.string());

schema.parse(1234); // => passes, as 1234 is not a string
schema.parse("hello"); // => throws an error, as "hello" is a string
```

For convenience, you can also call the `.not()` method on an existing schema.

```ts
const schema = z.string().email().not();

schema.parse(1234); // => passes, as 1234 is not a string or a valid email
schema.parse("hello"); // => passes, as "hello" is not a valid email
schema.parse("[email protected]"); // => throws an error, as "[email protected]" is a valid email
```

## Optionals

You can make any schema optional with `z.optional()`. This wraps the schema in a `ZodOptional` instance and returns the result.
Expand Down
126 changes: 126 additions & 0 deletions deno/lib/__tests__/not.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// @ts-ignore TS6133
import { expect } from "https://deno.land/x/[email protected]/mod.ts";
const test = Deno.test;

import * as z from "../index.ts";

/** Note
* Since z.never() allows no values, z.not(z.never()) should allow all values, including undefined. Hence, there are no failing test cases for notNever.
* There are no passing validation cases for notAny, as it should reject everything.
*/

// Set 1
const str = z.string().not();
const email = z.string().email().not();
const num = z.number().not();
const arr = z.array(z.string()).not();
const rec = z.record(z.string()).not();
const obj = z.object({ username: z.string() }).not();

// Set 2
const bigInt = z.bigint().not();
const bool = z.boolean().not();
const date = z.date().not();
const sym = z.symbol().not();
const undef = z.undefined().not();
const nul = z.null().not();
const voidSchema = z.void().not();
const anySchema = z.any().not();
const unknownSchema = z.unknown().not();
const neverSchema = z.never().not();

// Set 3
const notString = z.not(z.string());
const notNumber = z.not(z.number());
const notStringArray = z.not(z.array(z.string()));
const notRecord = z.not(z.record(z.string()));
const notObject = z.not(z.object({ username: z.string() }));

// Set 4
const notBigInt = z.not(z.bigint());
const notBoolean = z.not(z.boolean());
const notDate = z.not(z.date());
const notSymbol = z.not(z.symbol());
const notUndefined = z.not(z.undefined());
const notNull = z.not(z.null());
const notVoid = z.not(z.void());
const notAny = z.not(z.any());
const notUnknown = z.not(z.unknown());
const notNever = z.not(z.never());

test("passing validations", () => {
// Set 1
str.parse(1234);
email.parse(1234);
email.parse("abcd");
num.parse("1234");
arr.parse([1234]);
rec.parse({ key: 1234 });
obj.parse({ username: 1234 });

// Set 2
bigInt.parse(10);
bool.parse(1234);
date.parse("not a date");
sym.parse("1234");
undef.parse(null);
nul.parse(undefined);
voidSchema.parse(null);
neverSchema.parse(undefined); // `z.never()` allows no values, so `notNever` can accept any value.

// Set 3
notString.parse(1234);
notNumber.parse("1234");
notStringArray.parse([1234]);
notRecord.parse({ key: 1234 });
notObject.parse({ username: 1234 });

// Set 4
notBigInt.parse(10);
notBoolean.parse(1234);
notDate.parse("not a date");
notSymbol.parse("1234");
notUndefined.parse(null);
notNull.parse(undefined);
notVoid.parse(null);
notNever.parse(undefined); // `z.never()` allows no values, so `notNever` can accept any value.
});

test("failing validations", () => {
// Set 1
expect(() => str.parse("1234")).toThrow();
expect(() => email.parse("[email protected]")).toThrow();
expect(() => num.parse(1234)).toThrow();
expect(() => arr.parse(["1234"])).toThrow();
expect(() => rec.parse({ key: "value" })).toThrow();
expect(() => obj.parse({ username: "1234" })).toThrow();

// Set 2
expect(() => bigInt.parse(BigInt(10))).toThrow();
expect(() => bool.parse(true)).toThrow();
expect(() => date.parse(new Date())).toThrow();
expect(() => sym.parse(Symbol("symbol"))).toThrow();
expect(() => undef.parse(undefined)).toThrow();
expect(() => nul.parse(null)).toThrow();
expect(() => voidSchema.parse(undefined)).toThrow();
expect(() => anySchema.parse(undefined)).toThrow(); // `z.any()` allows any value, so `notAny` should fail for any input.
expect(() => unknownSchema.parse(undefined)).toThrow(); // Same as `z.any()`

// Set 3
expect(() => notString.parse("1234")).toThrow();
expect(() => notNumber.parse(1234)).toThrow();
expect(() => notStringArray.parse(["1234"])).toThrow();
expect(() => notRecord.parse({ key: "value" })).toThrow();
expect(() => notObject.parse({ username: "1234" })).toThrow();

// Set 4
expect(() => notBigInt.parse(BigInt(10))).toThrow();
expect(() => notBoolean.parse(true)).toThrow();
expect(() => notDate.parse(new Date())).toThrow();
expect(() => notSymbol.parse(Symbol("symbol"))).toThrow();
expect(() => notUndefined.parse(undefined)).toThrow();
expect(() => notNull.parse(null)).toThrow();
expect(() => notVoid.parse(undefined)).toThrow();
expect(() => notAny.parse(undefined)).toThrow(); // `z.any()` allows any value, so `notAny` should fail for any input.
expect(() => notUnknown.parse(undefined)).toThrow(); // Same as `z.any()`
});
63 changes: 63 additions & 0 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ export abstract class ZodType<
this.nullable = this.nullable.bind(this);
this.nullish = this.nullish.bind(this);
this.array = this.array.bind(this);
this.not = this.not.bind(this);
this.promise = this.promise.bind(this);
this.or = this.or.bind(this);
this.and = this.and.bind(this);
Expand All @@ -436,6 +437,9 @@ export abstract class ZodType<
array(): ZodArray<this> {
return ZodArray.create(this, this._def);
}
not(): ZodNot<this> {
return ZodNot.create(this, this._def);
}
promise(): ZodPromise<this> {
return ZodPromise.create(this, this._def);
}
Expand Down Expand Up @@ -2295,6 +2299,62 @@ export class ZodArray<
};
}

export interface ZodNotDef<T extends ZodTypeAny = ZodTypeAny>
extends ZodTypeDef {
item: T;
typeName: ZodFirstPartyTypeKind.ZodNot;
}

export class ZodNot<T extends ZodTypeAny> extends ZodType<
T["_output"] | null,
ZodNotDef<T>,
T["_input"] | null
> {
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
const { ctx } = this._processInputParams(input);

const schema = this._def.item;

if (!schema) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_arguments,
argumentsError: ctx.data,
});

return INVALID;
}

const result = schema._parse(
new ParseInputLazyPath(ctx, ctx.data, ctx.path, 0)
);

if (!!result && String((result as any).status) == "valid") {
addIssueToContext(ctx, {
code: ZodIssueCode.custom,
message: "Invalid input",
});

return INVALID;
}

return OK(input.data);
}

static create = <T extends ZodTypeAny>(
schemas: T,
params?: RawCreateParams
): ZodNot<T> => {
if (!schemas) {
throw new Error("You must pass a schema to z.not( ... )");
}
return new ZodNot({
item: schemas,
typeName: ZodFirstPartyTypeKind.ZodNot,
...processCreateParams(params),
});
};
}

export type ZodNonEmptyArray<T extends ZodTypeAny> = ZodArray<T, "atleastone">;

/////////////////////////////////////////
Expand Down Expand Up @@ -5125,6 +5185,7 @@ export enum ZodFirstPartyTypeKind {
ZodUndefined = "ZodUndefined",
ZodNull = "ZodNull",
ZodAny = "ZodAny",
ZodNot = "ZodNot",
ZodUnknown = "ZodUnknown",
ZodNever = "ZodNever",
ZodVoid = "ZodVoid",
Expand Down Expand Up @@ -5216,6 +5277,7 @@ const unknownType = ZodUnknown.create;
const neverType = ZodNever.create;
const voidType = ZodVoid.create;
const arrayType = ZodArray.create;
const notType = ZodNot.create;
const objectType = ZodObject.create;
const strictObjectType = ZodObject.strictCreate;
const unionType = ZodUnion.create;
Expand Down Expand Up @@ -5274,6 +5336,7 @@ export {
nanType as nan,
nativeEnumType as nativeEnum,
neverType as never,
notType as not,
nullType as null,
nullableType as nullable,
numberType as number,
Expand Down
Loading