From 04e1f379f6989d23dd45660fcabc78f76d7834f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20N=C3=A9meth?= Date: Thu, 2 May 2024 23:00:38 +0200 Subject: [PATCH] Fixed freezing async ZodReadonly results (#3457) * docs: Corrected typo in readme * test: result freezing after async parse * fix: result freezing after async parse --- README.md | 2 +- src/__tests__/readonly.test.ts | 50 ++++++++++++++++++++++++++++++++++ src/types.ts | 13 ++++++--- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3b6db2a9d..cccb50de3 100644 --- a/README.md +++ b/README.md @@ -2682,7 +2682,7 @@ Note that branded types do not affect the runtime result of `.parse`. It is a st This method returns a `ZodReadonly` schema instance that parses the input using the base schema, then calls `Object.freeze()` on the result. The inferred type is also marked as `readonly`. ```ts -const schema = z.object({ name: string }).readonly(); +const schema = z.object({ name: z.string() }).readonly(); type schema = z.infer; // Readonly<{name: string}> diff --git a/src/__tests__/readonly.test.ts b/src/__tests__/readonly.test.ts index 267ff190c..073bc533d 100644 --- a/src/__tests__/readonly.test.ts +++ b/src/__tests__/readonly.test.ts @@ -202,3 +202,53 @@ test("object freezing", () => { ) ).toBe(true); }); + +test("async object freezing", async () => { + expect( + Object.isFrozen(await z.array(z.string()).readonly().parseAsync(["a"])) + ).toBe(true); + expect( + Object.isFrozen( + await z.tuple([z.string(), z.number()]).readonly().parseAsync(["a", 1]) + ) + ).toBe(true); + expect( + Object.isFrozen( + await z + .map(z.string(), z.date()) + .readonly() + .parseAsync(new Map([["a", new Date()]])) + ) + ).toBe(true); + expect( + Object.isFrozen( + await z + .set(z.promise(z.string())) + .readonly() + .parseAsync(new Set([Promise.resolve("a")])) + ) + ).toBe(true); + expect( + Object.isFrozen( + await z.record(z.string()).readonly().parseAsync({ a: "b" }) + ) + ).toBe(true); + expect( + Object.isFrozen( + await z.record(z.string(), z.number()).readonly().parseAsync({ a: 1 }) + ) + ).toBe(true); + expect( + Object.isFrozen( + await z + .object({ a: z.string(), 1: z.number() }) + .readonly() + .parseAsync({ a: "b", 1: 2 }) + ) + ).toBe(true); + expect( + Object.isFrozen( + await z.promise(z.string()).readonly().parseAsync(Promise.resolve("a")) + ) + ).toBe(true); +}); diff --git a/src/types.ts b/src/types.ts index 2aec55df2..a5217d4a2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5041,10 +5041,15 @@ export class ZodReadonly extends ZodType< > { _parse(input: ParseInput): ParseReturnType { const result = this._def.innerType._parse(input); - if (isValid(result)) { - result.value = Object.freeze(result.value); - } - return result; + const freeze = (data: ParseReturnType) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) + ? result.then((data) => freeze(data)) + : freeze(result); } static create = (