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: strict branding with opt-out #3380

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
10 changes: 8 additions & 2 deletions deno/lib/__tests__/branded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ test("branded types", () => {
true
);

// keeping brands out of input types
const age = z.number().brand<"age">();
// keeping brands out of input types in non-strict mode
const age = z.number().brand<"age", false>();

type Age = z.infer<typeof age>;
type AgeInput = z.input<typeof age>;
Expand All @@ -59,4 +59,10 @@ test("branded types", () => {

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

// (default) strict mode - should be branded
const height = z.number().brand("metricHeight");

type Height = z.input<typeof height>;
util.assertEqual<number & z.BRAND<"metricHeight">, Height>(true);
});
6 changes: 4 additions & 2 deletions deno/lib/helpers/parseUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,11 @@ export class ParseStatus {
): Promise<SyncParseReturnType<any>> {
const syncPairs: ObjectPair[] = [];
for (const pair of pairs) {
const key = await pair.key;
const value = await pair.value;
syncPairs.push({
key: await pair.key,
value: await pair.value,
key,
value,
});
}
return ParseStatus.mergeObjectSync(status, syncPairs);
Expand Down
26 changes: 20 additions & 6 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,16 @@ 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, S extends boolean = true>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-breaking:

Suggested change
brand<B extends string | number | symbol, S extends boolean = true>(
brand<B extends string | number | symbol, S extends boolean = false>(

brand?: B,
opts?: {
strict: S;
}
): ZodBranded<this, B, S>;
brand<
B extends string | number | symbol,
S extends boolean = true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-breaking

Suggested change
S extends boolean = true
S extends boolean = false

>(): ZodBranded<this, B, S> {
return new ZodBranded({
typeName: ZodFirstPartyTypeKind.ZodBranded,
type: this,
Expand Down Expand Up @@ -2357,9 +2365,10 @@ export class ZodObject<
const syncPairs: any[] = [];
for (const pair of pairs) {
const key = await pair.key;
const value = await pair.value;
syncPairs.push({
key,
value: await pair.value,
value,
alwaysSet: pair.alwaysSet,
});
}
Expand Down Expand Up @@ -4726,8 +4735,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,
S extends boolean
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be

Suggested change
S extends boolean
S extends boolean = false

if we don't want breaking changes

> extends ZodType<
T["_output"] & BRAND<B>,
ZodBrandedDef<T>,
S extends true ? T["_input"] & BRAND<B> : T["_input"]
> {
_parse(input: ParseInput): ParseReturnType<any> {
const { ctx } = this._processInputParams(input);
const data = ctx.data;
Expand Down Expand Up @@ -4999,7 +5013,7 @@ export type ZodFirstPartySchemaTypes =
| ZodDefault<any>
| ZodCatch<any>
| ZodPromise<any>
| ZodBranded<any, any>
| ZodBranded<any, any, any>
| ZodPipeline<any, any>
| ZodReadonly<any>
| ZodSymbol;
Expand Down
10 changes: 8 additions & 2 deletions src/__tests__/branded.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ test("branded types", () => {
true
);

// keeping brands out of input types
const age = z.number().brand<"age">();
// keeping brands out of input types in non-strict mode
const age = z.number().brand<"age", false>();

type Age = z.infer<typeof age>;
type AgeInput = z.input<typeof age>;
Expand All @@ -58,4 +58,10 @@ test("branded types", () => {

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

// (default) strict mode - should be branded
const height = z.number().brand("metricHeight");

type Height = z.input<typeof height>;
util.assertEqual<number & z.BRAND<"metricHeight">, Height>(true);
});
23 changes: 18 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,16 @@ 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, S extends boolean = true>(
brand?: B,
opts?: {
strict: S;
}
): ZodBranded<this, B, S>;
brand<
B extends string | number | symbol,
S extends boolean = true
>(): ZodBranded<this, B, S> {
return new ZodBranded({
typeName: ZodFirstPartyTypeKind.ZodBranded,
type: this,
Expand Down Expand Up @@ -4727,8 +4735,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,
S extends boolean
> extends ZodType<
T["_output"] & BRAND<B>,
ZodBrandedDef<T>,
S extends true ? T["_input"] & BRAND<B> : T["_input"]
> {
_parse(input: ParseInput): ParseReturnType<any> {
const { ctx } = this._processInputParams(input);
const data = ctx.data;
Expand Down Expand Up @@ -5000,7 +5013,7 @@ export type ZodFirstPartySchemaTypes =
| ZodDefault<any>
| ZodCatch<any>
| ZodPromise<any>
| ZodBranded<any, any>
| ZodBranded<any, any, any>
| ZodPipeline<any, any>
| ZodReadonly<any>
| ZodSymbol;
Expand Down
Loading