Skip to content

Commit

Permalink
Add ZodReadonly (#2634)
Browse files Browse the repository at this point in the history
* Add ZodReadonly

* Use Bun in CI

* Fix link

* Update

* Fix prettier

* Update readme
  • Loading branch information
colinhacks authored Aug 14, 2023
1 parent 1ecd624 commit 981d4b5
Show file tree
Hide file tree
Showing 9 changed files with 624 additions and 12 deletions.
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ jobs:
- run: yarn build
- run: yarn test


test-deno:
runs-on: ubuntu-latest
strategy:
Expand Down
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@
- [`.or`](#or)
- [`.and`](#and)
- [`.brand`](#brand)
- [`.pipe()`](#pipe)
- [`.readonly`](#readonly)
- [`.pipe`](#pipe)
- [You can use `.pipe()` to fix common issues with `z.coerce`.](#you-can-use-pipe-to-fix-common-issues-with-zcoerce)
- [Guides and concepts](#guides-and-concepts)
- [Type inference](#type-inference)
Expand Down Expand Up @@ -2453,7 +2454,38 @@ type Cat = z.infer<typeof Cat>;

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

### `.pipe()`
### `.readonly`

`.readonly() => ZodReadonly<this>`

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();
type schema = z.infer<typeof schema>;
// Readonly<{name: string}>

const result = schema.parse({ name: "fido" });
result.name = "simba"; // error
```

The inferred type uses TypeScript's built-in readonly types when relevant.

```ts
z.array(z.string()).readonly();
// readonly string[]

z.tuple([z.string(), z.number()]).readonly();
// readonly [string, number]

z.map(z.string(), z.date()).readonly();
// ReadonlyMap<string, Date>

z.set(z.string()).readonly();
// ReadonlySet<Promise<string>>
```

### `.pipe`

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

Expand Down
36 changes: 34 additions & 2 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@
- [`.or`](#or)
- [`.and`](#and)
- [`.brand`](#brand)
- [`.pipe()`](#pipe)
- [`.readonly`](#readonly)
- [`.pipe`](#pipe)
- [You can use `.pipe()` to fix common issues with `z.coerce`.](#you-can-use-pipe-to-fix-common-issues-with-zcoerce)
- [Guides and concepts](#guides-and-concepts)
- [Type inference](#type-inference)
Expand Down Expand Up @@ -2453,7 +2454,38 @@ type Cat = z.infer<typeof Cat>;

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

### `.pipe()`
### `.readonly`

`.readonly() => ZodReadonly<this>`

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();
type schema = z.infer<typeof schema>;
// Readonly<{name: string}>

const result = schema.parse({ name: "fido" });
result.name = "simba"; // error
```

The inferred type uses TypeScript's built-in readonly types when relevant.

```ts
z.array(z.string()).readonly();
// readonly string[]

z.tuple([z.string(), z.number()]).readonly();
// readonly [string, number]

z.map(z.string(), z.date()).readonly();
// ReadonlyMap<string, Date>

z.set(z.string()).readonly();
// ReadonlySet<Promise<string>>
```

### `.pipe`

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

Expand Down
205 changes: 205 additions & 0 deletions deno/lib/__tests__/readonly.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// @ts-ignore TS6133
import { expect } from "https://deno.land/x/expect@v0.2.6/mod.ts";
const test = Deno.test;

import { util } from "../helpers/util.ts";
import * as z from "../index.ts";

enum testEnum {
A,
B,
}

const schemas = [
z.string().readonly(),
z.number().readonly(),
z.nan().readonly(),
z.bigint().readonly(),
z.boolean().readonly(),
z.date().readonly(),
z.undefined().readonly(),
z.null().readonly(),
z.any().readonly(),
z.unknown().readonly(),
z.void().readonly(),
z.function().args(z.string(), z.number()).readonly(),

z.array(z.string()).readonly(),
z.tuple([z.string(), z.number()]).readonly(),
z.map(z.string(), z.date()).readonly(),
z.set(z.promise(z.string())).readonly(),
z.record(z.string()).readonly(),
z.record(z.string(), z.number()).readonly(),
z.object({ a: z.string(), 1: z.number() }).readonly(),
z.nativeEnum(testEnum).readonly(),
z.promise(z.string()).readonly(),
] as const;

test("flat inference", () => {
util.assertEqual<z.infer<(typeof schemas)[0]>, string>(true);
util.assertEqual<z.infer<(typeof schemas)[1]>, number>(true);
util.assertEqual<z.infer<(typeof schemas)[2]>, number>(true);
util.assertEqual<z.infer<(typeof schemas)[3]>, bigint>(true);
util.assertEqual<z.infer<(typeof schemas)[4]>, boolean>(true);
util.assertEqual<z.infer<(typeof schemas)[5]>, Date>(true);
util.assertEqual<z.infer<(typeof schemas)[6]>, undefined>(true);
util.assertEqual<z.infer<(typeof schemas)[7]>, null>(true);
util.assertEqual<z.infer<(typeof schemas)[8]>, any>(true);
util.assertEqual<z.infer<(typeof schemas)[9]>, Readonly<unknown>>(true);
util.assertEqual<z.infer<(typeof schemas)[10]>, void>(true);
util.assertEqual<
z.infer<(typeof schemas)[11]>,
(args_0: string, args_1: number, ...args_2: unknown[]) => unknown
>(true);
util.assertEqual<z.infer<(typeof schemas)[12]>, readonly string[]>(true);

util.assertEqual<z.infer<(typeof schemas)[13]>, readonly [string, number]>(
true
);
util.assertEqual<z.infer<(typeof schemas)[14]>, ReadonlyMap<string, Date>>(
true
);
util.assertEqual<z.infer<(typeof schemas)[15]>, ReadonlySet<Promise<string>>>(
true
);
util.assertEqual<
z.infer<(typeof schemas)[16]>,
Readonly<Record<string, string>>
>(true);
util.assertEqual<
z.infer<(typeof schemas)[17]>,
Readonly<Record<string, number>>
>(true);
util.assertEqual<
z.infer<(typeof schemas)[18]>,
{ readonly a: string; readonly 1: number }
>(true);
util.assertEqual<z.infer<(typeof schemas)[19]>, Readonly<testEnum>>(true);
util.assertEqual<z.infer<(typeof schemas)[20]>, Promise<string>>(true);
});

// test("deep inference", () => {
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[0]>, string>(true);
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[1]>, number>(true);
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[2]>, number>(true);
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[3]>, bigint>(true);
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[4]>, boolean>(true);
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[5]>, Date>(true);
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[6]>, undefined>(true);
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[7]>, null>(true);
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[8]>, any>(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[9]>,
// Readonly<unknown>
// >(true);
// util.assertEqual<z.infer<(typeof deepReadonlySchemas_0)[10]>, void>(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[11]>,
// (args_0: string, args_1: number, ...args_2: unknown[]) => unknown
// >(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[12]>,
// readonly string[]
// >(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[13]>,
// readonly [string, number]
// >(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[14]>,
// ReadonlyMap<string, Date>
// >(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[15]>,
// ReadonlySet<Promise<string>>
// >(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[16]>,
// Readonly<Record<string, string>>
// >(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[17]>,
// Readonly<Record<string, number>>
// >(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[18]>,
// { readonly a: string; readonly 1: number }
// >(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[19]>,
// Readonly<testEnum>
// >(true);
// util.assertEqual<
// z.infer<(typeof deepReadonlySchemas_0)[20]>,
// Promise<string>
// >(true);

// util.assertEqual<
// z.infer<typeof crazyDeepReadonlySchema>,
// ReadonlyMap<
// ReadonlySet<readonly [string, number]>,
// {
// readonly a: {
// readonly [x: string]: readonly any[];
// };
// readonly b: {
// readonly c: {
// readonly d: {
// readonly e: {
// readonly f: {
// readonly g?: {};
// };
// };
// };
// };
// };
// }
// >
// >(true);
// });

test("object freezing", () => {
expect(Object.isFrozen(z.array(z.string()).readonly().parse(["a"]))).toBe(
true
);
expect(
Object.isFrozen(
z.tuple([z.string(), z.number()]).readonly().parse(["a", 1])
)
).toBe(true);
expect(
Object.isFrozen(
z
.map(z.string(), z.date())
.readonly()
.parse(new Map([["a", new Date()]]))
)
).toBe(true);
expect(
Object.isFrozen(
z
.set(z.promise(z.string()))
.readonly()
.parse(new Set([Promise.resolve("a")]))
)
).toBe(true);
expect(
Object.isFrozen(z.record(z.string()).readonly().parse({ a: "b" }))
).toBe(true);
expect(
Object.isFrozen(z.record(z.string(), z.number()).readonly().parse({ a: 1 }))
).toBe(true);
expect(
Object.isFrozen(
z
.object({ a: z.string(), 1: z.number() })
.readonly()
.parse({ a: "b", 1: 2 })
)
).toBe(true);
expect(
Object.isFrozen(
z.promise(z.string()).readonly().parse(Promise.resolve("a"))
)
).toBe(true);
});
Loading

0 comments on commit 981d4b5

Please sign in to comment.