Skip to content

Commit 981d4b5

Browse files
authored
Add ZodReadonly (#2634)
* Add ZodReadonly * Use Bun in CI * Fix link * Update * Fix prettier * Update readme
1 parent 1ecd624 commit 981d4b5

File tree

9 files changed

+624
-12
lines changed

9 files changed

+624
-12
lines changed

.github/workflows/test.yml

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ jobs:
2626
- run: yarn build
2727
- run: yarn test
2828

29-
3029
test-deno:
3130
runs-on: ubuntu-latest
3231
strategy:

README.md

+34-2
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@
143143
- [`.or`](#or)
144144
- [`.and`](#and)
145145
- [`.brand`](#brand)
146-
- [`.pipe()`](#pipe)
146+
- [`.readonly`](#readonly)
147+
- [`.pipe`](#pipe)
147148
- [You can use `.pipe()` to fix common issues with `z.coerce`.](#you-can-use-pipe-to-fix-common-issues-with-zcoerce)
148149
- [Guides and concepts](#guides-and-concepts)
149150
- [Type inference](#type-inference)
@@ -2453,7 +2454,38 @@ type Cat = z.infer<typeof Cat>;
24532454

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

2456-
### `.pipe()`
2457+
### `.readonly`
2458+
2459+
`.readonly() => ZodReadonly<this>`
2460+
2461+
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`.
2462+
2463+
```ts
2464+
const schema = z.object({ name: string }).readonly();
2465+
type schema = z.infer<typeof schema>;
2466+
// Readonly<{name: string}>
2467+
2468+
const result = schema.parse({ name: "fido" });
2469+
result.name = "simba"; // error
2470+
```
2471+
2472+
The inferred type uses TypeScript's built-in readonly types when relevant.
2473+
2474+
```ts
2475+
z.array(z.string()).readonly();
2476+
// readonly string[]
2477+
2478+
z.tuple([z.string(), z.number()]).readonly();
2479+
// readonly [string, number]
2480+
2481+
z.map(z.string(), z.date()).readonly();
2482+
// ReadonlyMap<string, Date>
2483+
2484+
z.set(z.string()).readonly();
2485+
// ReadonlySet<Promise<string>>
2486+
```
2487+
2488+
### `.pipe`
24572489

24582490
Schemas can be chained into validation "pipelines". It's useful for easily validating the result after a `.transform()`:
24592491

deno/lib/README.md

+34-2
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@
143143
- [`.or`](#or)
144144
- [`.and`](#and)
145145
- [`.brand`](#brand)
146-
- [`.pipe()`](#pipe)
146+
- [`.readonly`](#readonly)
147+
- [`.pipe`](#pipe)
147148
- [You can use `.pipe()` to fix common issues with `z.coerce`.](#you-can-use-pipe-to-fix-common-issues-with-zcoerce)
148149
- [Guides and concepts](#guides-and-concepts)
149150
- [Type inference](#type-inference)
@@ -2453,7 +2454,38 @@ type Cat = z.infer<typeof Cat>;
24532454

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

2456-
### `.pipe()`
2457+
### `.readonly`
2458+
2459+
`.readonly() => ZodReadonly<this>`
2460+
2461+
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`.
2462+
2463+
```ts
2464+
const schema = z.object({ name: string }).readonly();
2465+
type schema = z.infer<typeof schema>;
2466+
// Readonly<{name: string}>
2467+
2468+
const result = schema.parse({ name: "fido" });
2469+
result.name = "simba"; // error
2470+
```
2471+
2472+
The inferred type uses TypeScript's built-in readonly types when relevant.
2473+
2474+
```ts
2475+
z.array(z.string()).readonly();
2476+
// readonly string[]
2477+
2478+
z.tuple([z.string(), z.number()]).readonly();
2479+
// readonly [string, number]
2480+
2481+
z.map(z.string(), z.date()).readonly();
2482+
// ReadonlyMap<string, Date>
2483+
2484+
z.set(z.string()).readonly();
2485+
// ReadonlySet<Promise<string>>
2486+
```
2487+
2488+
### `.pipe`
24572489

24582490
Schemas can be chained into validation "pipelines". It's useful for easily validating the result after a `.transform()`:
24592491

deno/lib/__tests__/readonly.test.ts

+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// @ts-ignore TS6133
2+
import { expect } from "https://deno.land/x/[email protected]/mod.ts";
3+
const test = Deno.test;
4+
5+
import { util } from "../helpers/util.ts";
6+
import * as z from "../index.ts";
7+
8+
enum testEnum {
9+
A,
10+
B,
11+
}
12+
13+
const schemas = [
14+
z.string().readonly(),
15+
z.number().readonly(),
16+
z.nan().readonly(),
17+
z.bigint().readonly(),
18+
z.boolean().readonly(),
19+
z.date().readonly(),
20+
z.undefined().readonly(),
21+
z.null().readonly(),
22+
z.any().readonly(),
23+
z.unknown().readonly(),
24+
z.void().readonly(),
25+
z.function().args(z.string(), z.number()).readonly(),
26+
27+
z.array(z.string()).readonly(),
28+
z.tuple([z.string(), z.number()]).readonly(),
29+
z.map(z.string(), z.date()).readonly(),
30+
z.set(z.promise(z.string())).readonly(),
31+
z.record(z.string()).readonly(),
32+
z.record(z.string(), z.number()).readonly(),
33+
z.object({ a: z.string(), 1: z.number() }).readonly(),
34+
z.nativeEnum(testEnum).readonly(),
35+
z.promise(z.string()).readonly(),
36+
] as const;
37+
38+
test("flat inference", () => {
39+
util.assertEqual<z.infer<(typeof schemas)[0]>, string>(true);
40+
util.assertEqual<z.infer<(typeof schemas)[1]>, number>(true);
41+
util.assertEqual<z.infer<(typeof schemas)[2]>, number>(true);
42+
util.assertEqual<z.infer<(typeof schemas)[3]>, bigint>(true);
43+
util.assertEqual<z.infer<(typeof schemas)[4]>, boolean>(true);
44+
util.assertEqual<z.infer<(typeof schemas)[5]>, Date>(true);
45+
util.assertEqual<z.infer<(typeof schemas)[6]>, undefined>(true);
46+
util.assertEqual<z.infer<(typeof schemas)[7]>, null>(true);
47+
util.assertEqual<z.infer<(typeof schemas)[8]>, any>(true);
48+
util.assertEqual<z.infer<(typeof schemas)[9]>, Readonly<unknown>>(true);
49+
util.assertEqual<z.infer<(typeof schemas)[10]>, void>(true);
50+
util.assertEqual<
51+
z.infer<(typeof schemas)[11]>,
52+
(args_0: string, args_1: number, ...args_2: unknown[]) => unknown
53+
>(true);
54+
util.assertEqual<z.infer<(typeof schemas)[12]>, readonly string[]>(true);
55+
56+
util.assertEqual<z.infer<(typeof schemas)[13]>, readonly [string, number]>(
57+
true
58+
);
59+
util.assertEqual<z.infer<(typeof schemas)[14]>, ReadonlyMap<string, Date>>(
60+
true
61+
);
62+
util.assertEqual<z.infer<(typeof schemas)[15]>, ReadonlySet<Promise<string>>>(
63+
true
64+
);
65+
util.assertEqual<
66+
z.infer<(typeof schemas)[16]>,
67+
Readonly<Record<string, string>>
68+
>(true);
69+
util.assertEqual<
70+
z.infer<(typeof schemas)[17]>,
71+
Readonly<Record<string, number>>
72+
>(true);
73+
util.assertEqual<
74+
z.infer<(typeof schemas)[18]>,
75+
{ readonly a: string; readonly 1: number }
76+
>(true);
77+
util.assertEqual<z.infer<(typeof schemas)[19]>, Readonly<testEnum>>(true);
78+
util.assertEqual<z.infer<(typeof schemas)[20]>, Promise<string>>(true);
79+
});
80+
81+
// test("deep inference", () => {
82+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[0]>, string>(true);
83+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[1]>, number>(true);
84+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[2]>, number>(true);
85+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[3]>, bigint>(true);
86+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[4]>, boolean>(true);
87+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[5]>, Date>(true);
88+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[6]>, undefined>(true);
89+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[7]>, null>(true);
90+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[8]>, any>(true);
91+
// util.assertEqual<
92+
// z.infer<(typeof deepReadonlySchemas_0)[9]>,
93+
// Readonly<unknown>
94+
// >(true);
95+
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[10]>, void>(true);
96+
// util.assertEqual<
97+
// z.infer<(typeof deepReadonlySchemas_0)[11]>,
98+
// (args_0: string, args_1: number, ...args_2: unknown[]) => unknown
99+
// >(true);
100+
// util.assertEqual<
101+
// z.infer<(typeof deepReadonlySchemas_0)[12]>,
102+
// readonly string[]
103+
// >(true);
104+
// util.assertEqual<
105+
// z.infer<(typeof deepReadonlySchemas_0)[13]>,
106+
// readonly [string, number]
107+
// >(true);
108+
// util.assertEqual<
109+
// z.infer<(typeof deepReadonlySchemas_0)[14]>,
110+
// ReadonlyMap<string, Date>
111+
// >(true);
112+
// util.assertEqual<
113+
// z.infer<(typeof deepReadonlySchemas_0)[15]>,
114+
// ReadonlySet<Promise<string>>
115+
// >(true);
116+
// util.assertEqual<
117+
// z.infer<(typeof deepReadonlySchemas_0)[16]>,
118+
// Readonly<Record<string, string>>
119+
// >(true);
120+
// util.assertEqual<
121+
// z.infer<(typeof deepReadonlySchemas_0)[17]>,
122+
// Readonly<Record<string, number>>
123+
// >(true);
124+
// util.assertEqual<
125+
// z.infer<(typeof deepReadonlySchemas_0)[18]>,
126+
// { readonly a: string; readonly 1: number }
127+
// >(true);
128+
// util.assertEqual<
129+
// z.infer<(typeof deepReadonlySchemas_0)[19]>,
130+
// Readonly<testEnum>
131+
// >(true);
132+
// util.assertEqual<
133+
// z.infer<(typeof deepReadonlySchemas_0)[20]>,
134+
// Promise<string>
135+
// >(true);
136+
137+
// util.assertEqual<
138+
// z.infer<typeof crazyDeepReadonlySchema>,
139+
// ReadonlyMap<
140+
// ReadonlySet<readonly [string, number]>,
141+
// {
142+
// readonly a: {
143+
// readonly [x: string]: readonly any[];
144+
// };
145+
// readonly b: {
146+
// readonly c: {
147+
// readonly d: {
148+
// readonly e: {
149+
// readonly f: {
150+
// readonly g?: {};
151+
// };
152+
// };
153+
// };
154+
// };
155+
// };
156+
// }
157+
// >
158+
// >(true);
159+
// });
160+
161+
test("object freezing", () => {
162+
expect(Object.isFrozen(z.array(z.string()).readonly().parse(["a"]))).toBe(
163+
true
164+
);
165+
expect(
166+
Object.isFrozen(
167+
z.tuple([z.string(), z.number()]).readonly().parse(["a", 1])
168+
)
169+
).toBe(true);
170+
expect(
171+
Object.isFrozen(
172+
z
173+
.map(z.string(), z.date())
174+
.readonly()
175+
.parse(new Map([["a", new Date()]]))
176+
)
177+
).toBe(true);
178+
expect(
179+
Object.isFrozen(
180+
z
181+
.set(z.promise(z.string()))
182+
.readonly()
183+
.parse(new Set([Promise.resolve("a")]))
184+
)
185+
).toBe(true);
186+
expect(
187+
Object.isFrozen(z.record(z.string()).readonly().parse({ a: "b" }))
188+
).toBe(true);
189+
expect(
190+
Object.isFrozen(z.record(z.string(), z.number()).readonly().parse({ a: 1 }))
191+
).toBe(true);
192+
expect(
193+
Object.isFrozen(
194+
z
195+
.object({ a: z.string(), 1: z.number() })
196+
.readonly()
197+
.parse({ a: "b", 1: 2 })
198+
)
199+
).toBe(true);
200+
expect(
201+
Object.isFrozen(
202+
z.promise(z.string()).readonly().parse(Promise.resolve("a"))
203+
)
204+
).toBe(true);
205+
});

0 commit comments

Comments
 (0)