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 brand to ZodBranded def at runtime #2860

Closed
wants to merge 1 commit into from
Closed
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2442,6 +2442,17 @@ type Cat = z.infer<typeof Cat>;

Note that branded types do not affect the runtime result of `.parse`. It is a static-only construct.

However when using the brand as a parameter the type will include it in the typing at runtime (but not the parse!)

```ts
const Cat = z.object({ name: z.string() }).brand("Cat"); // Notice brand being a parameter
type Cat = z.infer<typeof Cat>;
// {name: string} & {[symbol]: "Cat"}

const isCatType = Cat._def.brand === "Cat"; // true
const isCat = Cat.parse({ name: "Whiskers" })._def.brand; // Invalid. This is undefined
```

### `.readonly`

`.readonly() => ZodReadonly<this>`
Expand Down
13 changes: 12 additions & 1 deletion deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1879,7 +1879,7 @@ You can create a Zod schema for any TypeScript type by using `z.custom()`. This

```ts
const px = z.custom<`${number}px`>((val) => {
return /^\d+px$/.test(val as string);
return typeof val === "string" ? /^\d+px$/.test(val) : false;
});

type px = z.infer<typeof px>; // `${number}px`
Expand Down Expand Up @@ -2442,6 +2442,17 @@ type Cat = z.infer<typeof Cat>;

Note that branded types do not affect the runtime result of `.parse`. It is a static-only construct.

However when using the brand as a parameter the type will include it in the typing at runtime (but not the parse!)

```ts
const Cat = z.object({ name: z.string() }).brand("Cat"); // Notice brand being a parameter
type Cat = z.infer<typeof Cat>;
// {name: string} & {[symbol]: "Cat"}

const isCatType = Cat._def.brand === "Cat"; // true
const isCat = Cat.parse({ name: "Whiskers" })._def.brand; // Invalid. This is undefined
```

### `.readonly`

`.readonly() => ZodReadonly<this>`
Expand Down
13 changes: 13 additions & 0 deletions deno/lib/__tests__/branded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,17 @@ test("branded types", () => {

// @ts-expect-error
doStuff({ name: "hello there!" });

// runtime brand
const runtimeBranded = z.number().brand("runtime");
expect(runtimeBranded.parse(3000)).toEqual(3000);
expect(runtimeBranded._def.brand).toEqual("runtime");

const runtimeBrandedWithSymbol = z.string().brand(MyBrand);
expect(runtimeBrandedWithSymbol.parse("hi")).toEqual("hi");
expect(runtimeBrandedWithSymbol._def.brand).toEqual(MyBrand);

const typeBranded = z.string().brand<'myType'>();
expect(typeBranded.parse("hi")).toEqual("hi");
expect(typeBranded._def.brand).toEqual(undefined);
});
25 changes: 19 additions & 6 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,13 @@ export abstract class ZodType<
}) as any;
}

brand<B extends string | number | symbol>(brand?: B): ZodBranded<this, B>;
brand<B extends string | number | symbol>(): ZodBranded<this, B> {
brand<B extends string | number | symbol>(): ZodBranded<this, B, undefined>;
brand<B extends string | number | symbol>(brand: B): ZodBranded<this, B, B>;
brand<B extends string | number | symbol>(
brand?: B
): ZodBranded<this, B, B | undefined> {
return new ZodBranded({
brand,
typeName: ZodFirstPartyTypeKind.ZodBranded,
type: this,
...processCreateParams(this._def),
Expand Down Expand Up @@ -4678,9 +4682,13 @@ export class ZodNaN extends ZodType<number, ZodNaNDef> {
//////////////////////////////////////////
//////////////////////////////////////////

export interface ZodBrandedDef<T extends ZodTypeAny> extends ZodTypeDef {
export interface ZodBrandedDef<
T extends ZodTypeAny,
B extends string | number | symbol | undefined
> extends ZodTypeDef {
type: T;
typeName: ZodFirstPartyTypeKind.ZodBranded;
brand: B;
}

export const BRAND: unique symbol = Symbol("zod_brand");
Expand All @@ -4690,8 +4698,13 @@ export type BRAND<T extends string | number | symbol> = {

export class ZodBranded<
T extends ZodTypeAny,
B extends string | number | symbol
> extends ZodType<T["_output"] & BRAND<B>, ZodBrandedDef<T>, T["_input"]> {
B extends string | number | symbol,
BRuntime extends B | undefined = undefined
> extends ZodType<
T["_output"] & BRAND<B>,
ZodBrandedDef<T, BRuntime>,
T["_input"]
> {
_parse(input: ParseInput): ParseReturnType<any> {
const { ctx } = this._processInputParams(input);
const data = ctx.data;
Expand Down Expand Up @@ -4959,7 +4972,7 @@ export type ZodFirstPartySchemaTypes =
| ZodDefault<any>
| ZodCatch<any>
| ZodPromise<any>
| ZodBranded<any, any>
| ZodBranded<any, any, any>
| ZodPipeline<any, any>;

// requires TS 4.4+
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/branded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,17 @@ test("branded types", () => {

// @ts-expect-error
doStuff({ name: "hello there!" });

// runtime brand
const runtimeBranded = z.number().brand("runtime");
expect(runtimeBranded.parse(3000)).toEqual(3000);
expect(runtimeBranded._def.brand).toEqual("runtime");

const runtimeBrandedWithSymbol = z.string().brand(MyBrand);
expect(runtimeBrandedWithSymbol.parse("hi")).toEqual("hi");
expect(runtimeBrandedWithSymbol._def.brand).toEqual(MyBrand);

const typeBranded = z.string().brand<'myType'>();
expect(typeBranded.parse("hi")).toEqual("hi");
expect(typeBranded._def.brand).toEqual(undefined);
});
25 changes: 19 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,13 @@ export abstract class ZodType<
}) as any;
}

brand<B extends string | number | symbol>(brand?: B): ZodBranded<this, B>;
brand<B extends string | number | symbol>(): ZodBranded<this, B> {
brand<B extends string | number | symbol>(): ZodBranded<this, B, undefined>;
brand<B extends string | number | symbol>(brand: B): ZodBranded<this, B, B>;
brand<B extends string | number | symbol>(
brand?: B
): ZodBranded<this, B, B | undefined> {
return new ZodBranded({
brand,
typeName: ZodFirstPartyTypeKind.ZodBranded,
type: this,
...processCreateParams(this._def),
Expand Down Expand Up @@ -4678,9 +4682,13 @@ export class ZodNaN extends ZodType<number, ZodNaNDef> {
//////////////////////////////////////////
//////////////////////////////////////////

export interface ZodBrandedDef<T extends ZodTypeAny> extends ZodTypeDef {
export interface ZodBrandedDef<
T extends ZodTypeAny,
B extends string | number | symbol | undefined
> extends ZodTypeDef {
type: T;
typeName: ZodFirstPartyTypeKind.ZodBranded;
brand: B;
}

export const BRAND: unique symbol = Symbol("zod_brand");
Expand All @@ -4690,8 +4698,13 @@ export type BRAND<T extends string | number | symbol> = {

export class ZodBranded<
T extends ZodTypeAny,
B extends string | number | symbol
> extends ZodType<T["_output"] & BRAND<B>, ZodBrandedDef<T>, T["_input"]> {
B extends string | number | symbol,
BRuntime extends B | undefined = undefined
> extends ZodType<
T["_output"] & BRAND<B>,
ZodBrandedDef<T, BRuntime>,
T["_input"]
> {
_parse(input: ParseInput): ParseReturnType<any> {
const { ctx } = this._processInputParams(input);
const data = ctx.data;
Expand Down Expand Up @@ -4959,7 +4972,7 @@ export type ZodFirstPartySchemaTypes =
| ZodDefault<any>
| ZodCatch<any>
| ZodPromise<any>
| ZodBranded<any, any>
| ZodBranded<any, any, any>
| ZodPipeline<any, any>;

// requires TS 4.4+
Expand Down