Skip to content

Commit

Permalink
Merge pull request #1 from botpress/merge-add-zodtemplateliteral-igal…
Browse files Browse the repository at this point in the history
…klebanov

Merge add zodtemplateliteral igalklebanov
  • Loading branch information
charlescatta authored Apr 3, 2024
2 parents 1748dde + 1ce249c commit fe43cb2
Show file tree
Hide file tree
Showing 12 changed files with 2,661 additions and 4 deletions.
119 changes: 119 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
- [Promises](#promises)
- [Instanceof](#instanceof)
- [Functions](#functions)
- [Template Literals](#template-literals)
- [Preprocess](#preprocess)
- [Custom schemas](#custom-schemas)
- [Schema methods](#schema-methods)
Expand Down Expand Up @@ -2020,6 +2021,124 @@ myFunction.returnType();
* `args: ZodTuple` The first argument is a tuple (created with `z.tuple([...])` and defines the schema of the arguments to your function. If the function doesn't accept arguments, you can pass an empty tuple (`z.tuple([])`).
* `returnType: any Zod schema` The second argument is the function's return type. This can be any Zod schema. -->

## Template Literals

Building on the knowledge above, Zod supports creating typescript [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) with runtime validation. These types allow for stricter type checking of string inputs, as an alternative to `z.string()` which infers to a string.

A template literal type consists of [string literal types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) and interpolated positions (typescript types inside `${}` slots, e.g. `${number}`).

To create a template literal builder:

```ts
const templateLiteral = z.templateLiteral(); // infers to ``.
```

- To add string literal types to an existing template literal:

```ts
templateLiteral.literal("Hello"); // infers to `Hello`.
templateLiteral.literal(3.14); // infers to `3.14`.
```

This method accepts strings, numbers, booleans, nulls and undefined.

- To add interpolated positions to an existing template literal:

```ts
templateLiteral.interpolated(z.string()); // infers to `${string}`.
templateLiteral.interpolated(z.number()); // infers to `${number}`.
templateLiteral.interpolated(z.boolean()); // infers to `true` | `false`.
templateLiteral.interpolated(z.literal("foo")); // infers to `foo`.
templateLiteral.interpolated(z.null()); // infers to `null`.
templateLiteral.interpolated(z.undefined()); // infers to `undefined`.
templateLiteral.interpolated(z.bigint()); // infers to `${bigint}`.
templateLiteral.interpolated(z.any()); // infers to `${any}`.
```

Any Zod type (or union) with an underlying type of string, number, boolean, null,
undefined or bigint can be used as an interpolated position (template literals
included!). You can use additional built-in runtime validations (refinements
excluded) in each of these types and the template literal builder will do its
best (within the limitations of regular expressions) to support them when parsing.

### Examples

URL:

```ts
const url = z
.templateLiteral()
.literal("https://")
.interpolated(z.string().min(1))
.literal(".")
.interpolated(z.enum(["com", "net"]));
// infers to `https://${string}.com` | `https://${string}.net`.

url.parse("https://google.com"); // passes
url.parse("https://google.net"); // passes
url.parse("http://google.com"); // throws
url.parse("https://.com"); // throws
url.parse("https://google"); // throws
url.parse("https://google."); // throws
url.parse("https://google.gov"); // throws
```

Measurement:

```ts
const measurement = z.coerce
.templateLiteral()
.interpolated(z.number().finite())
.interpolated(
z.enum(["px", "em", "rem", "vh", "vw", "vmin", "vmax"]).optional()
);
// infers to `${number}` | `${number}px` | `${number}em` | `${number}rem` | `${number}vh` | `${number}vw` | `${number}vmin` | `${number}vmax
```

MongoDB connection string:

```ts
const connectionString = z
.templateLiteral()
.literal("mongodb://")
.interpolated(
z
.templateLiteral()
.interpolated(z.string().regex(/\w+/).describe("username"))
.literal(":")
.interpolated(z.string().regex(/\w+/).describe("password"))
.literal("@")
.optional()
)
.interpolated(z.string().regex(/\w+/).describe("host"))
.literal(":")
.interpolated(z.number().finite().int().positive().describe("port"))
.interpolated(
z
.templateLiteral()
.literal("/")
.interpolated(
z.string().regex(/\w+/).optional().describe("defaultauthdb")
)
.interpolated(
z
.templateLiteral()
.literal("?")
.interpolated(z.string().regex(/^\w+=\w+(&\w+=\w+)*$/))
.optional()
.describe("options")
)
.optional()
);
// infers to:
// | `mongodb://${string}:${number}`
// | `mongodb://${string}:${number}/${string}`
// | `mongodb://${string}:${number}/${string}?${string}`
// | `mongodb://${string}:${string}@${string}:${number}`
// | `mongodb://${string}:${string}@${string}:${number}/${string}`
// | `mongodb://${string}:${string}@${string}:${number}/${string}?${string}`
```

## Preprocess

> Zod now supports primitive coercion without the need for `.preprocess()`. See the [coercion docs](#coercion-for-primitives) for more information.
Expand Down
119 changes: 119 additions & 0 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
- [Promises](#promises)
- [Instanceof](#instanceof)
- [Functions](#functions)
- [Template Literals](#template-literals)
- [Preprocess](#preprocess)
- [Custom schemas](#custom-schemas)
- [Schema methods](#schema-methods)
Expand Down Expand Up @@ -2020,6 +2021,124 @@ myFunction.returnType();
* `args: ZodTuple` The first argument is a tuple (created with `z.tuple([...])` and defines the schema of the arguments to your function. If the function doesn't accept arguments, you can pass an empty tuple (`z.tuple([])`).
* `returnType: any Zod schema` The second argument is the function's return type. This can be any Zod schema. -->

## Template Literals

Building on the knowledge above, Zod supports creating typescript [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) with runtime validation. These types allow for stricter type checking of string inputs, as an alternative to `z.string()` which infers to a string.

A template literal type consists of [string literal types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) and interpolated positions (typescript types inside `${}` slots, e.g. `${number}`).

To create a template literal builder:

```ts
const templateLiteral = z.templateLiteral(); // infers to ``.
```

- To add string literal types to an existing template literal:

```ts
templateLiteral.literal("Hello"); // infers to `Hello`.
templateLiteral.literal(3.14); // infers to `3.14`.
```

This method accepts strings, numbers, booleans, nulls and undefined.

- To add interpolated positions to an existing template literal:

```ts
templateLiteral.interpolated(z.string()); // infers to `${string}`.
templateLiteral.interpolated(z.number()); // infers to `${number}`.
templateLiteral.interpolated(z.boolean()); // infers to `true` | `false`.
templateLiteral.interpolated(z.literal("foo")); // infers to `foo`.
templateLiteral.interpolated(z.null()); // infers to `null`.
templateLiteral.interpolated(z.undefined()); // infers to `undefined`.
templateLiteral.interpolated(z.bigint()); // infers to `${bigint}`.
templateLiteral.interpolated(z.any()); // infers to `${any}`.
```

Any Zod type (or union) with an underlying type of string, number, boolean, null,
undefined or bigint can be used as an interpolated position (template literals
included!). You can use additional built-in runtime validations (refinements
excluded) in each of these types and the template literal builder will do its
best (within the limitations of regular expressions) to support them when parsing.

### Examples

URL:

```ts
const url = z
.templateLiteral()
.literal("https://")
.interpolated(z.string().min(1))
.literal(".")
.interpolated(z.enum(["com", "net"]));
// infers to `https://${string}.com` | `https://${string}.net`.

url.parse("https://google.com"); // passes
url.parse("https://google.net"); // passes
url.parse("http://google.com"); // throws
url.parse("https://.com"); // throws
url.parse("https://google"); // throws
url.parse("https://google."); // throws
url.parse("https://google.gov"); // throws
```

Measurement:

```ts
const measurement = z.coerce
.templateLiteral()
.interpolated(z.number().finite())
.interpolated(
z.enum(["px", "em", "rem", "vh", "vw", "vmin", "vmax"]).optional()
);
// infers to `${number}` | `${number}px` | `${number}em` | `${number}rem` | `${number}vh` | `${number}vw` | `${number}vmin` | `${number}vmax
```

MongoDB connection string:

```ts
const connectionString = z
.templateLiteral()
.literal("mongodb://")
.interpolated(
z
.templateLiteral()
.interpolated(z.string().regex(/\w+/).describe("username"))
.literal(":")
.interpolated(z.string().regex(/\w+/).describe("password"))
.literal("@")
.optional()
)
.interpolated(z.string().regex(/\w+/).describe("host"))
.literal(":")
.interpolated(z.number().finite().int().positive().describe("port"))
.interpolated(
z
.templateLiteral()
.literal("/")
.interpolated(
z.string().regex(/\w+/).optional().describe("defaultauthdb")
)
.interpolated(
z
.templateLiteral()
.literal("?")
.interpolated(z.string().regex(/^\w+=\w+(&\w+=\w+)*$/))
.optional()
.describe("options")
)
.optional()
);
// infers to:
// | `mongodb://${string}:${number}`
// | `mongodb://${string}:${number}/${string}`
// | `mongodb://${string}:${number}/${string}?${string}`
// | `mongodb://${string}:${string}@${string}:${number}`
// | `mongodb://${string}:${string}@${string}:${number}/${string}`
// | `mongodb://${string}:${string}@${string}:${number}/${string}?${string}`
```

## Preprocess

> Zod now supports primitive coercion without the need for `.preprocess()`. See the [coercion docs](#coercion-for-primitives) for more information.
Expand Down
34 changes: 33 additions & 1 deletion deno/lib/ZodError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TypeOf, ZodType } from "./index.ts";
import type { TypeOf, ZodFirstPartyTypeKind, ZodType } from "./index.ts";
import { Primitive } from "./helpers/typeAliases.ts";
import { util, ZodParsedType } from "./helpers/util.ts";

Expand Down Expand Up @@ -332,3 +332,35 @@ export type ZodErrorMap = (
issue: ZodIssueOptionalMessage,
_ctx: ErrorMapCtx
) => { message: string };

export class ZodTemplateLiteralUnsupportedTypeError extends Error {
constructor() {
super("Unsupported zod type!");

const actualProto = new.target.prototype;
if (Object.setPrototypeOf) {
// eslint-disable-next-line ban/ban
Object.setPrototypeOf(this, actualProto);
} else {
(this as any).__proto__ = actualProto;
}
this.name = "ZodTemplateLiteralUnsupportedTypeError";
}
}

export class ZodTemplateLiteralUnsupportedCheckError extends Error {
constructor(typeKind: ZodFirstPartyTypeKind, check: string) {
super(
`${typeKind}'s "${check}" check is not supported in template literals!`
);

const actualProto = new.target.prototype;
if (Object.setPrototypeOf) {
// eslint-disable-next-line ban/ban
Object.setPrototypeOf(this, actualProto);
} else {
(this as any).__proto__ = actualProto;
}
this.name = "ZodTemplateLiteralUnsupportedCheckError";
}
}
20 changes: 20 additions & 0 deletions deno/lib/__tests__/coerce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,23 @@ test("date coercion", () => {
expect(() => schema.parse([])).toThrow(); // z.ZodError
expect(schema.parse(new Date())).toBeInstanceOf(Date);
});

test("template literal coercion", () => {
const schema = z.coerce
.templateLiteral()
.interpolated(z.number().finite())
.interpolated(
z.enum(["px", "em", "rem", "vh", "vw", "vmin", "vmax"]).optional()
);
expect(schema.parse(300)).toEqual("300");
expect(schema.parse(BigInt(300))).toEqual("300");
expect(schema.parse("300")).toEqual("300");
expect(schema.parse("300px")).toEqual("300px");
expect(schema.parse("300em")).toEqual("300em");
expect(schema.parse("300rem")).toEqual("300rem");
expect(schema.parse("300vh")).toEqual("300vh");
expect(schema.parse("300vw")).toEqual("300vw");
expect(schema.parse("300vmin")).toEqual("300vmin");
expect(schema.parse("300vmax")).toEqual("300vmax");
expect(schema.parse(["300px"])).toEqual("300px");
});
2 changes: 2 additions & 0 deletions deno/lib/__tests__/firstparty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodReadonly:
break;
case z.ZodFirstPartyTypeKind.ZodTemplateLiteral:
break;
default:
util.assertNever(def);
}
Expand Down
Loading

0 comments on commit fe43cb2

Please sign in to comment.