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/add guard #3862

Open
wants to merge 4 commits 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
6 changes: 5 additions & 1 deletion configs/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ export default defineConfig({
alias: {
"@jest/globals": "vitest",
},
include: ["src/**/*.test.ts"],
include: ["src/**/*.test.ts", "src/**/*.test-d.ts"],
isolate: false,
watch: false,
typecheck: {
enabled: true,
include: ["src/**/*.test-d.ts"],
},
},
});
47 changes: 47 additions & 0 deletions deno/lib/__tests__/guard.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expectTypeOf, test } from "vitest";
import { z } from "../index.ts";

describe("guard", () => {
test("work as a type guard when Output extends Input", () => {
expectTypeOf<true>().toEqualTypeOf<true>();
});
test("work as a type guard when Output *does* extend Input", () => {
const inputExtendsOutputSchema = z.object({
a: z.string(),
b: z.enum(["x", "y"]).transform((arg) => arg as string),
});
const val = null as unknown;
if (inputExtendsOutputSchema.guard(val)) {
expectTypeOf(val).toEqualTypeOf<{ a: string; b: string }>();
} else {
expectTypeOf(val).toEqualTypeOf<unknown>();
}
});

test("to be unavailable when Output *does not* extend Input", () => {
const inputDiffersFromOutputSchema = z
.string()
.transform((arg) => parseFloat(arg));
const val = null as unknown;
// @ts-expect-error - compile error as Input does not extend Output
inputDiffersFromOutputSchema.guard(val);
});

test("coerce to impact the Import", () => {
const schemaA = z.coerce.string();
type InputA = (typeof schemaA)["_input"];
expectTypeOf({} as InputA).toEqualTypeOf<unknown>();
expectTypeOf({} as InputA).not.toEqualTypeOf<string>();
// @ts-expect-error - compile error as Input does not extend Output
schemaA.guard({});

const schemaB = z.date({
coerce: true,
});
type InputB = (typeof schemaB)["_input"];
expectTypeOf({} as InputB).toEqualTypeOf<unknown>();
expectTypeOf({} as InputB).not.toEqualTypeOf<Date>();
// @ts-expect-error - compile error as Input does not extend Output
schemaB.guard({});
});
});
127 changes: 79 additions & 48 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,10 @@ export abstract class ZodType<
isNullable(): boolean {
return this.safeParse(null).success;
}

guard(this: ZodType<Output>, data: unknown): data is Output {
return this.safeParse(data).success;
}
}

/////////////////////////////////////////
Expand Down Expand Up @@ -567,10 +571,11 @@ export type ZodStringCheck =
| { kind: "ip"; version?: IpVersion; message?: string }
| { kind: "base64"; message?: string };

export interface ZodStringDef extends ZodTypeDef {
export interface ZodStringDef<TCoerce extends boolean = false>
extends ZodTypeDef {
checks: ZodStringCheck[];
typeName: ZodFirstPartyTypeKind.ZodString;
coerce: boolean;
coerce: TCoerce;
}

const cuidRegex = /^c[^\s-]{8,}$/i;
Expand Down Expand Up @@ -671,7 +676,11 @@ function isValidIP(ip: string, version?: IpVersion) {
return false;
}

export class ZodString extends ZodType<string, ZodStringDef, string> {
export class ZodString<TCoerce extends boolean = false> extends ZodType<
string,
ZodStringDef<TCoerce>,
TCoerce extends true ? unknown : string
> {
_parse(input: ParseInput): ParseReturnType<string> {
if (this._def.coerce) {
input.data = String(input.data);
Expand Down Expand Up @@ -1223,11 +1232,13 @@ export class ZodString extends ZodType<string, ZodStringDef, string> {
return max;
}

static create = (params?: RawCreateParams & { coerce?: true }): ZodString => {
return new ZodString({
static create = <TParam extends RawCreateParams & { coerce?: true }>(
params?: TParam
): ZodString<TParam["coerce"] extends true ? true : false> => {
return new ZodString<TParam["coerce"] extends true ? true : false>({
checks: [],
typeName: ZodFirstPartyTypeKind.ZodString,
coerce: params?.coerce ?? false,
coerce: params?.coerce ?? (false as any), // TODO remove need for 'as any'
...processCreateParams(params),
});
};
Expand Down Expand Up @@ -1257,13 +1268,18 @@ function floatSafeRemainder(val: number, step: number) {
return (valInt % stepInt) / Math.pow(10, decCount);
}

export interface ZodNumberDef extends ZodTypeDef {
export interface ZodNumberDef<TCoerce extends boolean = false>
extends ZodTypeDef {
checks: ZodNumberCheck[];
typeName: ZodFirstPartyTypeKind.ZodNumber;
coerce: boolean;
coerce: TCoerce;
}

export class ZodNumber extends ZodType<number, ZodNumberDef, number> {
export class ZodNumber<TCoerce extends boolean = false> extends ZodType<
number,
ZodNumberDef<TCoerce>,
TCoerce extends true ? unknown : number
> {
_parse(input: ParseInput): ParseReturnType<number> {
if (this._def.coerce) {
input.data = Number(input.data);
Expand Down Expand Up @@ -1353,13 +1369,13 @@ export class ZodNumber extends ZodType<number, ZodNumberDef, number> {
return { status: status.value, value: input.data };
}

static create = (
params?: RawCreateParams & { coerce?: boolean }
): ZodNumber => {
return new ZodNumber({
static create = <TParam extends RawCreateParams & { coerce?: boolean }>(
params?: TParam
): ZodNumber<TParam["coerce"] extends true ? true : false> => {
return new ZodNumber<TParam["coerce"] extends true ? true : false>({
checks: [],
typeName: ZodFirstPartyTypeKind.ZodNumber,
coerce: params?.coerce || false,
coerce: params?.coerce ?? (false as any), // TODO remove need for 'as any'
...processCreateParams(params),
});
};
Expand Down Expand Up @@ -1542,13 +1558,18 @@ export type ZodBigIntCheck =
| { kind: "max"; value: bigint; inclusive: boolean; message?: string }
| { kind: "multipleOf"; value: bigint; message?: string };

export interface ZodBigIntDef extends ZodTypeDef {
export interface ZodBigIntDef<TCoerce extends boolean = false>
extends ZodTypeDef {
checks: ZodBigIntCheck[];
typeName: ZodFirstPartyTypeKind.ZodBigInt;
coerce: boolean;
coerce: TCoerce;
}

export class ZodBigInt extends ZodType<bigint, ZodBigIntDef, bigint> {
export class ZodBigInt<TCoerce extends boolean = false> extends ZodType<
bigint,
ZodBigIntDef<TCoerce>,
TCoerce extends true ? unknown : bigint
> {
_parse(input: ParseInput): ParseReturnType<bigint> {
if (this._def.coerce) {
input.data = BigInt(input.data);
Expand Down Expand Up @@ -1616,13 +1637,13 @@ export class ZodBigInt extends ZodType<bigint, ZodBigIntDef, bigint> {
return { status: status.value, value: input.data };
}

static create = (
params?: RawCreateParams & { coerce?: boolean }
): ZodBigInt => {
return new ZodBigInt({
static create = <TParam extends RawCreateParams & { coerce?: true }>(
params?: TParam
): ZodBigInt<TParam["coerce"] extends true ? true : false> => {
return new ZodBigInt<TParam["coerce"] extends true ? true : false>({
checks: [],
typeName: ZodFirstPartyTypeKind.ZodBigInt,
coerce: params?.coerce ?? false,
coerce: params?.coerce ?? (false as any), // TODO remove need for 'as any'
...processCreateParams(params),
});
};
Expand Down Expand Up @@ -1744,12 +1765,17 @@ export class ZodBigInt extends ZodType<bigint, ZodBigIntDef, bigint> {
////////// ///////////
//////////////////////////////////////////
//////////////////////////////////////////
export interface ZodBooleanDef extends ZodTypeDef {
export interface ZodBooleanDef<TCoerce extends boolean = false>
extends ZodTypeDef {
typeName: ZodFirstPartyTypeKind.ZodBoolean;
coerce: boolean;
coerce: TCoerce;
}

export class ZodBoolean extends ZodType<boolean, ZodBooleanDef, boolean> {
export class ZodBoolean<TCoerce extends boolean = false> extends ZodType<
boolean,
ZodBooleanDef<TCoerce>,
TCoerce extends true ? unknown : boolean
> {
_parse(input: ParseInput): ParseReturnType<boolean> {
if (this._def.coerce) {
input.data = Boolean(input.data);
Expand All @@ -1768,12 +1794,12 @@ export class ZodBoolean extends ZodType<boolean, ZodBooleanDef, boolean> {
return OK(input.data);
}

static create = (
params?: RawCreateParams & { coerce?: boolean }
): ZodBoolean => {
return new ZodBoolean({
static create = <TParam extends RawCreateParams & { coerce?: true }>(
params?: TParam
): ZodBoolean<TParam["coerce"] extends true ? true : false> => {
return new ZodBoolean<TParam["coerce"] extends true ? true : false>({
typeName: ZodFirstPartyTypeKind.ZodBoolean,
coerce: params?.coerce || false,
coerce: params?.coerce ?? (false as any), // TODO remove need for 'as any'
...processCreateParams(params),
});
};
Expand All @@ -1789,13 +1815,18 @@ export class ZodBoolean extends ZodType<boolean, ZodBooleanDef, boolean> {
export type ZodDateCheck =
| { kind: "min"; value: number; message?: string }
| { kind: "max"; value: number; message?: string };
export interface ZodDateDef extends ZodTypeDef {
export interface ZodDateDef<TCoerce extends boolean = false>
extends ZodTypeDef {
checks: ZodDateCheck[];
coerce: boolean;
coerce: TCoerce;
typeName: ZodFirstPartyTypeKind.ZodDate;
}

export class ZodDate extends ZodType<Date, ZodDateDef, Date> {
export class ZodDate<TCoerce extends boolean = false> extends ZodType<
Date,
ZodDateDef<TCoerce>,
TCoerce extends true ? unknown : Date
> {
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
if (this._def.coerce) {
input.data = new Date(input.data);
Expand Down Expand Up @@ -1906,12 +1937,12 @@ export class ZodDate extends ZodType<Date, ZodDateDef, Date> {
return max != null ? new Date(max) : null;
}

static create = (
params?: RawCreateParams & { coerce?: boolean }
): ZodDate => {
return new ZodDate({
static create = <TParam extends RawCreateParams & { coerce?: true }>(
params?: TParam
): ZodDate<TParam["coerce"] extends true ? true : false> => {
return new ZodDate<TParam["coerce"] extends true ? true : false>({
checks: [],
coerce: params?.coerce || false,
coerce: params?.coerce ?? (false as any), // TODO remove need for 'as any'
typeName: ZodFirstPartyTypeKind.ZodDate,
...processCreateParams(params),
});
Expand Down Expand Up @@ -5243,19 +5274,19 @@ const onumber = () => numberType().optional();
const oboolean = () => booleanType().optional();

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

export {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"tslib": "^2.3.1",
"tsx": "^3.8.0",
"typescript": "~4.5.5",
"vitest": "^0.32.2"
"vitest": "^2.1.5"
},
"exports": {
".": {
Expand Down
47 changes: 47 additions & 0 deletions src/__tests__/guard.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expectTypeOf, test } from "vitest";
import { z } from "../index";

describe("guard", () => {
test("work as a type guard when Output extends Input", () => {
expectTypeOf<true>().toEqualTypeOf<true>();
});
test("work as a type guard when Output *does* extend Input", () => {
const inputExtendsOutputSchema = z.object({
a: z.string(),
b: z.enum(["x", "y"]).transform((arg) => arg as string),
});
const val = null as unknown;
if (inputExtendsOutputSchema.guard(val)) {
expectTypeOf(val).toEqualTypeOf<{ a: string; b: string }>();
} else {
expectTypeOf(val).toEqualTypeOf<unknown>();
}
});

test("to be unavailable when Output *does not* extend Input", () => {
const inputDiffersFromOutputSchema = z
.string()
.transform((arg) => parseFloat(arg));
const val = null as unknown;
// @ts-expect-error - compile error as Input does not extend Output
inputDiffersFromOutputSchema.guard(val);
});

test("coerce to impact the Import", () => {
const schemaA = z.coerce.string();
type InputA = (typeof schemaA)["_input"];
expectTypeOf({} as InputA).toEqualTypeOf<unknown>();
expectTypeOf({} as InputA).not.toEqualTypeOf<string>();
// @ts-expect-error - compile error as Input does not extend Output
schemaA.guard({});

const schemaB = z.date({
coerce: true,
});
type InputB = (typeof schemaB)["_input"];
expectTypeOf({} as InputB).toEqualTypeOf<unknown>();
expectTypeOf({} as InputB).not.toEqualTypeOf<Date>();
// @ts-expect-error - compile error as Input does not extend Output
schemaB.guard({});
});
});
Loading