Skip to content

Commit

Permalink
Fix recursion bugs. ZodTransformer nesting is no longer banned.
Browse files Browse the repository at this point in the history
  • Loading branch information
Colin McDonnell committed Jun 13, 2021
1 parent 41a70c3 commit 12ae009
Show file tree
Hide file tree
Showing 10 changed files with 44 additions and 137 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

### 3.2

- Certain methods (`.or`, `.transform`) now return a new instance that wrap the current instance, instead of trying to avoid additional nesting. For example:

```ts
z.union([z.string(), z.number()]).or(z.boolean());
// previously
// => ZodUnion<[ZodString, ZodNumber, ZodBoolean]>

// now
// => ZodUnion<[ZodUnion<[ZodString, ZodNumber]>, ZodBoolean]>
```

This change was made due to recursion limitations in TypeScript 4.3 that made it impossible to properly type these methods.

### 3.0.0-beta.1

- Moved default value logic into ZodDefault. Implemented `.nullish()` method.
Expand Down
2 changes: 1 addition & 1 deletion coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 1 addition & 14 deletions deno/lib/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,8 @@ export type DenormalizedError = { [k: string]: DenormalizedError | string[] };

export type ZodIssueOptionalMessage =
| ZodInvalidTypeIssue
// | ZodNonEmptyArrayIsEmptyIssue
| ZodUnrecognizedKeysIssue
| ZodInvalidUnionIssue
// | ZodInvalidLiteralValueIssue
| ZodInvalidEnumValueIssue
| ZodInvalidArgumentsIssue
| ZodInvalidReturnTypeIssue
Expand All @@ -110,7 +108,7 @@ export type ZodIssueOptionalMessage =
export type ZodIssue = ZodIssueOptionalMessage & { message: string };

export const quotelessJson = (obj: any) => {
const json = JSON.stringify(obj, null, 2); // {"name":"John Smith"}
const json = JSON.stringify(obj, null, 2);
return json.replace(/"([^"]+)":/g, "$1:");
};

Expand All @@ -125,17 +123,6 @@ export type ZodFormattedError<T> = { _errors: string[] } & (T extends [
? { [K in keyof T]?: ZodFormattedError<T[K]> }
: { _errors: string[] });

// type t2 = ZodFormattedError<string>;
// type asdf = ZodFormattedError<{ outer: { asdf: string } }>;

// export type ZodFormattedError<T> = T extends [any, ...any]
// ? { [K in keyof T]?: ZodFormattedError<T[K]> }
// : T extends any[]
// ? ZodFormattedError<T[number]>[]
// : T extends object
// ? { [K in keyof T]?: ZodFormattedError<T[K]> }
// : { _errors: string[] };

export class ZodError<T = any> extends Error {
issues: ZodIssue[] = [];

Expand Down
6 changes: 0 additions & 6 deletions deno/lib/__tests__/transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,3 @@ test("multiple transformers", () => {
});
expect(doubler.parse("5")).toEqual(10);
});

test("no nesting", () => {
expect(() => {
z.transformer(z.transformer(z.string()));
}).toThrow();
});
6 changes: 0 additions & 6 deletions deno/lib/playground.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { z } from "./index.ts";
const run = async () => {
z;
const a = z.string().transform((arg) => arg.toLowerCase());
const b = a.transform((arg) => arg.length);
b;
// const infer = <C extends z.ZodTypeAny>(arg: C) => {
// return arg.transform((val) => ({ val }));
// };
};

run();
Expand Down
55 changes: 13 additions & 42 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,17 +277,6 @@ export abstract class ZodType<
});
};

// _refinement: (refinement: InternalCheck<Output>["refinement"]) => this = (
// refinement
// ) => {
// return new (this as any).constructor({
// ...this._def,
// effects: [
// // ...(this._def.effects || []),
// { type: "check", check: refinement },
// ],
// }) as this;
// };
_refinement<This extends this>(
refinement: InternalCheck<Output>["refinement"]
): ZodEffectsType<This> {
Expand Down Expand Up @@ -327,32 +316,20 @@ export abstract class ZodType<
array: () => ZodArray<this> = () => ZodArray.create(this);

or<T extends ZodTypeAny>(option: T): ZodUnion<[this, T]> {
// : ZodUnion<[This, T]> // : never // ? ZodUnion<[...Opts, T]> // ? [...Opts, T] extends ZodUnionOptions // : This extends ZodUnion<infer Opts>
// if (this instanceof ZodUnion) {
// return ZodUnion.create([...this.options, option] as any) as any;
// }
return ZodUnion.create([this, option]) as any;
}

and<T extends ZodTypeAny>(incoming: T): ZodIntersection<this, T> {
return ZodIntersection.create(this, incoming);
}

transform<NewOut, Inner extends ZodTypeAny = this>(
transform<NewOut>(
transform: (arg: Output) => NewOut | Promise<NewOut>
): ZodEffects<Inner, NewOut> {
// This extends ZodEffects<infer T, any>
// ? ZodEffects<Inner, NewOut>
// : ZodEffects<This, NewOut> {
): ZodEffects<this, NewOut> {
return new ZodEffects({
schema: this,
effects: [{ type: "transform", transform }],
}) as any;

// return new ZodEffects({
// schema: this,
// effects: [{ type: "transform", transform }],
// }) as any;
}

default<This extends this = this>(
Expand Down Expand Up @@ -2589,14 +2566,14 @@ export class ZodEffects<
return this._def.schema;
}

transform<NewOut, Inner extends ZodTypeAny = T>(
transform: (arg: Output) => NewOut | Promise<NewOut>
): ZodEffects<Inner, NewOut> {
return new ZodEffects({
...this._def,
effects: [...(this._def.effects || []), { type: "transform", transform }],
}) as any;
}
// transform<NewOut, Inner extends ZodTypeAny = T>(
// transform: (arg: Output) => NewOut | Promise<NewOut>
// ): ZodEffects<Inner, NewOut> {
// return new ZodEffects({
// ...this._def,
// effects: [...(this._def.effects || []), { type: "transform", transform }],
// }) as any;
// }

_parse(ctx: ParseContext): any {
const isSync = ctx.async === false || this instanceof ZodPromise;
Expand Down Expand Up @@ -2635,12 +2612,6 @@ export class ZodEffects<
PseudoPromise.resolve(data),
PseudoPromise.resolve(data).then(() => {
const result = effect.refinement(data, checkCtx);
// try {
// result = effect.refinement(data, checkCtx);
// } catch (err) {
// throw err;
// // if (refinementError === null) refinementError = err;
// }

if (isSync && result instanceof Promise)
throw new Error(
Expand Down Expand Up @@ -2683,9 +2654,9 @@ export class ZodEffects<

constructor(def: ZodEffectsDef<T>) {
super(def);
if (def.schema instanceof ZodEffects) {
throw new Error("ZodEffects cannot be nested.");
}
// if (def.schema instanceof ZodEffects) {
// throw new Error("ZodEffects cannot be nested.");
// }
}

static create = <I extends ZodTypeAny>(
Expand Down
15 changes: 1 addition & 14 deletions src/ZodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,8 @@ export type DenormalizedError = { [k: string]: DenormalizedError | string[] };

export type ZodIssueOptionalMessage =
| ZodInvalidTypeIssue
// | ZodNonEmptyArrayIsEmptyIssue
| ZodUnrecognizedKeysIssue
| ZodInvalidUnionIssue
// | ZodInvalidLiteralValueIssue
| ZodInvalidEnumValueIssue
| ZodInvalidArgumentsIssue
| ZodInvalidReturnTypeIssue
Expand All @@ -110,7 +108,7 @@ export type ZodIssueOptionalMessage =
export type ZodIssue = ZodIssueOptionalMessage & { message: string };

export const quotelessJson = (obj: any) => {
const json = JSON.stringify(obj, null, 2); // {"name":"John Smith"}
const json = JSON.stringify(obj, null, 2);
return json.replace(/"([^"]+)":/g, "$1:");
};

Expand All @@ -125,17 +123,6 @@ export type ZodFormattedError<T> = { _errors: string[] } & (T extends [
? { [K in keyof T]?: ZodFormattedError<T[K]> }
: { _errors: string[] });

// type t2 = ZodFormattedError<string>;
// type asdf = ZodFormattedError<{ outer: { asdf: string } }>;

// export type ZodFormattedError<T> = T extends [any, ...any]
// ? { [K in keyof T]?: ZodFormattedError<T[K]> }
// : T extends any[]
// ? ZodFormattedError<T[number]>[]
// : T extends object
// ? { [K in keyof T]?: ZodFormattedError<T[K]> }
// : { _errors: string[] };

export class ZodError<T = any> extends Error {
issues: ZodIssue[] = [];

Expand Down
6 changes: 0 additions & 6 deletions src/__tests__/transformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,3 @@ test("multiple transformers", () => {
});
expect(doubler.parse("5")).toEqual(10);
});

test("no nesting", () => {
expect(() => {
z.transformer(z.transformer(z.string()));
}).toThrow();
});
6 changes: 0 additions & 6 deletions src/playground.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { z } from "./index";
const run = async () => {
z;
const a = z.string().transform((arg) => arg.toLowerCase());
const b = a.transform((arg) => arg.length);
b;
// const infer = <C extends z.ZodTypeAny>(arg: C) => {
// return arg.transform((val) => ({ val }));
// };
};

run();
Expand Down
55 changes: 13 additions & 42 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,17 +277,6 @@ export abstract class ZodType<
});
};

// _refinement: (refinement: InternalCheck<Output>["refinement"]) => this = (
// refinement
// ) => {
// return new (this as any).constructor({
// ...this._def,
// effects: [
// // ...(this._def.effects || []),
// { type: "check", check: refinement },
// ],
// }) as this;
// };
_refinement<This extends this>(
refinement: InternalCheck<Output>["refinement"]
): ZodEffectsType<This> {
Expand Down Expand Up @@ -327,32 +316,20 @@ export abstract class ZodType<
array: () => ZodArray<this> = () => ZodArray.create(this);

or<T extends ZodTypeAny>(option: T): ZodUnion<[this, T]> {
// : ZodUnion<[This, T]> // : never // ? ZodUnion<[...Opts, T]> // ? [...Opts, T] extends ZodUnionOptions // : This extends ZodUnion<infer Opts>
// if (this instanceof ZodUnion) {
// return ZodUnion.create([...this.options, option] as any) as any;
// }
return ZodUnion.create([this, option]) as any;
}

and<T extends ZodTypeAny>(incoming: T): ZodIntersection<this, T> {
return ZodIntersection.create(this, incoming);
}

transform<NewOut, Inner extends ZodTypeAny = this>(
transform<NewOut>(
transform: (arg: Output) => NewOut | Promise<NewOut>
): ZodEffects<Inner, NewOut> {
// This extends ZodEffects<infer T, any>
// ? ZodEffects<Inner, NewOut>
// : ZodEffects<This, NewOut> {
): ZodEffects<this, NewOut> {
return new ZodEffects({
schema: this,
effects: [{ type: "transform", transform }],
}) as any;

// return new ZodEffects({
// schema: this,
// effects: [{ type: "transform", transform }],
// }) as any;
}

default<This extends this = this>(
Expand Down Expand Up @@ -2589,14 +2566,14 @@ export class ZodEffects<
return this._def.schema;
}

transform<NewOut, Inner extends ZodTypeAny = T>(
transform: (arg: Output) => NewOut | Promise<NewOut>
): ZodEffects<Inner, NewOut> {
return new ZodEffects({
...this._def,
effects: [...(this._def.effects || []), { type: "transform", transform }],
}) as any;
}
// transform<NewOut, Inner extends ZodTypeAny = T>(
// transform: (arg: Output) => NewOut | Promise<NewOut>
// ): ZodEffects<Inner, NewOut> {
// return new ZodEffects({
// ...this._def,
// effects: [...(this._def.effects || []), { type: "transform", transform }],
// }) as any;
// }

_parse(ctx: ParseContext): any {
const isSync = ctx.async === false || this instanceof ZodPromise;
Expand Down Expand Up @@ -2635,12 +2612,6 @@ export class ZodEffects<
PseudoPromise.resolve(data),
PseudoPromise.resolve(data).then(() => {
const result = effect.refinement(data, checkCtx);
// try {
// result = effect.refinement(data, checkCtx);
// } catch (err) {
// throw err;
// // if (refinementError === null) refinementError = err;
// }

if (isSync && result instanceof Promise)
throw new Error(
Expand Down Expand Up @@ -2683,9 +2654,9 @@ export class ZodEffects<

constructor(def: ZodEffectsDef<T>) {
super(def);
if (def.schema instanceof ZodEffects) {
throw new Error("ZodEffects cannot be nested.");
}
// if (def.schema instanceof ZodEffects) {
// throw new Error("ZodEffects cannot be nested.");
// }
}

static create = <I extends ZodTypeAny>(
Expand Down

0 comments on commit 12ae009

Please sign in to comment.