Skip to content

Commit

Permalink
Add z.literal.template (#1786)
Browse files Browse the repository at this point in the history
* `ZodTemplateLiteral` initial commit.

* `ZodTemplateLiteral` regex building & parsing.

* add `ZodTemplateLiteral` @ firstparty.test.

* some inference test cases for `ZodTemplateLiteral`.

* append to regexString instead of rebuild @ `ZodTemplateLiteral.addPart`.

* initial parse tests for `ZodTemplateLiteral`.

* "internalize" `addPart`, add `addLiteral` & `addInterpolatedPosition`.

* move `ZodTemplateLiteral` unit test cases and minor fixes.

* minor fixes, more tests.

* minor fixes, more tests pt. 2.

* more tests pt. 3.

* more tests pt. 4.

* add `ZodTemplateLiteral` README.md section.

* add a simpler example @ ZodTemplateLiteral docs.

* add regex limitations remark.

* allow coercion to ZodTemplateLiteral.

* minor readability pass on number stuff @ ZodTemplateLiteral.

* add ZodBranded support @ ZodTemplateLiteral.

* support ZodAny @ ZodTemplateLiteral.

* support ZodAny @ ZodTemplateLiteral.

* minor README.md changes.

* add ZodTemplateLiteral coerce test.

* add ZodTemplateLiteral custom errors for unsupported stuff.

* add ZodTemplateLiteral custom errors for unsupported stuff. pt. 2.

* add ZodTemplateLiteral custom errors for unsupported stuff pt. 3.

Co-authored-by: Max Arturo <[email protected]>

* use official MDN way of escaping for regex.

Co-authored-by: Max Arturo <[email protected]>

* explicitly state exponent notation is not supported @ ZodTemplateLiteral

Co-authored-by: Max Arturo <[email protected]>

* add missing `.toThrow()` @ ZodTemplateLiteral tests.

Co-authored-by: Max Arturo <[email protected]>

* explicitly state `.trim()` is not supported @ ZodTemplateLiteral.

Co-authored-by: Max Arturo <[email protected]>

* fix mongodb connection string example and tests.

* add measurment example from README to ZodTemplateLiteral tests.

* extract ZodTemplateLiteral errors to ZodError.ts

Co-authored-by: Max Arturo <[email protected]>

* add `z.string().cuid2()` support.

* add support for new regex based string checks.

* rename to `.interpolated` & `.literal`.

* rename to `.interpolated` & `.literal` pt. 2.

* handle case insensitive regexes.

* prettier the readme.

* Update cuid2 test

* Switch to z.literal.template, update tests, update eslint, bump TS to 5.0+

* Empty

* Update readme

* Update readme

* Clean up types

---------

Co-authored-by: Max Arturo <[email protected]>
Co-authored-by: Colin McDonnell <[email protected]>
3 people authored May 9, 2024
1 parent aac94cc commit 5b532d8
Showing 18 changed files with 2,952 additions and 258 deletions.
Empty file removed .eslintignore
Empty file.
120 changes: 120 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -119,6 +119,7 @@
- [Promises](#promises)
- [Instanceof](#instanceof)
- [Functions](#functions)
- [Template Literals](#template-literals)
- [Preprocess](#preprocess)
- [Custom schemas](#custom-schemas)
- [Schema methods](#schema-methods)
@@ -1947,6 +1948,125 @@ 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

TypeScript supports [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html), which are strings that conform to a statically known structure.

```ts
type simpleTemplate = `Hello, ${string}!`;
type urlTemplate = `${"http" | "https"}://${string}.${`com` | `net`}`;
type pxTemplate = `${number}px`;
```

These types can be represented in Zod with `z.literal.template()`. Template literals consist of interleaved _literals_ and _schemas_.

```ts
z.literal.template(["Hello, ", z.string()]); // infers to `Hello ${string}`
```

The literal components can be any string, number, boolean, null, or undefined.

```ts
z.literal.template(["Hello", 3.14, true, null, undefined]);
// infers to `Hello3.14truenullundefined`
```

The schema components can be any literal, primitive, or enum schema.

> **Note** — Refinements, transforms, and pipelines are not supported.
```ts
z.template.literal([
z.string(),
z.number(),
z.boolean(),
z.bigint(),
z.any(),
z.literal("foo"),
z.null(),
z.undefined(),
z.enum(["bar"]),
]);
```

For "union" types like `z.boolean()` or `z.enum()`, the inferred static type will be a union of the possible values.

```ts
z.literal.template([z.boolean(), z.number()]);
// `true${number}` | `false${number}`

z.literal.template(["is_", z.enum(["red", "green", "blue"])]);
// `is_red` | `is_green` | `is_blue`
```

### Examples

URL:

```ts
const url = z.literal.template([
"https://",
z.string(),
".",
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
```

CSS Measurement:

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

MongoDB connection string:

```ts
const connectionString = z.literal.template([
"mongodb://",
z.literal
.template([
z.string().regex(/\w+/).describe("username"),
":",
z.string().regex(/\w+/).describe("password"),
"@",
])
.optional(),
z.string().regex(/\w+/).describe("host"),
":",
z.number().finite().int().positive().describe("port"),
z.literal
.template([
"/",
z.string().regex(/\w+/).optional().describe("defaultauthdb"),
z.literal
.template(["?", z.string().regex(/^\w+=\w+(&\w+=\w+)*$/)])
.optional()
.describe("options"),
])
.optional(),
]);
// inferred type:
// | `mongodb://${string}:${number}`
// | `mongodb://${string}:${string}@${string}:${number}`
// | `mongodb://${string}:${number}/${string}`
// | `mongodb://${string}:${string}@${string}:${number}/${string}`
// | `mongodb://${string}:${number}/${string}?${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.
120 changes: 120 additions & 0 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
@@ -119,6 +119,7 @@
- [Promises](#promises)
- [Instanceof](#instanceof)
- [Functions](#functions)
- [Template Literals](#template-literals)
- [Preprocess](#preprocess)
- [Custom schemas](#custom-schemas)
- [Schema methods](#schema-methods)
@@ -1947,6 +1948,125 @@ 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

TypeScript supports [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html), which are strings that conform to a statically known structure.

```ts
type simpleTemplate = `Hello, ${string}!`;
type urlTemplate = `${"http" | "https"}://${string}.${`com` | `net`}`;
type pxTemplate = `${number}px`;
```

These types can be represented in Zod with `z.literal.template()`. Template literals consist of interleaved _literals_ and _schemas_.

```ts
z.literal.template(["Hello, ", z.string()]); // infers to `Hello ${string}`
```

The literal components can be any string, number, boolean, null, or undefined.

```ts
z.literal.template(["Hello", 3.14, true, null, undefined]);
// infers to `Hello3.14truenullundefined`
```

The schema components can be any literal, primitive, or enum schema.

> **Note** — Refinements, transforms, and pipelines are not supported.
```ts
z.template.literal([
z.string(),
z.number(),
z.boolean(),
z.bigint(),
z.any(),
z.literal("foo"),
z.null(),
z.undefined(),
z.enum(["bar"]),
]);
```

For "union" types like `z.boolean()` or `z.enum()`, the inferred static type will be a union of the possible values.

```ts
z.literal.template([z.boolean(), z.number()]);
// `true${number}` | `false${number}`

z.literal.template(["is_", z.enum(["red", "green", "blue"])]);
// `is_red` | `is_green` | `is_blue`
```

### Examples

URL:

```ts
const url = z.literal.template([
"https://",
z.string(),
".",
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
```

CSS Measurement:

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

MongoDB connection string:

```ts
const connectionString = z.literal.template([
"mongodb://",
z.literal
.template([
z.string().regex(/\w+/).describe("username"),
":",
z.string().regex(/\w+/).describe("password"),
"@",
])
.optional(),
z.string().regex(/\w+/).describe("host"),
":",
z.number().finite().int().positive().describe("port"),
z.literal
.template([
"/",
z.string().regex(/\w+/).optional().describe("defaultauthdb"),
z.literal
.template(["?", z.string().regex(/^\w+=\w+(&\w+=\w+)*$/)])
.optional()
.describe("options"),
])
.optional(),
]);
// inferred type:
// | `mongodb://${string}:${number}`
// | `mongodb://${string}:${string}@${string}:${number}`
// | `mongodb://${string}:${number}/${string}`
// | `mongodb://${string}:${string}@${string}:${number}/${string}`
// | `mongodb://${string}:${number}/${string}?${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.
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 { input, TypeOf, ZodType } from "./index.ts";
import type { input, TypeOf, ZodFirstPartyTypeKind, ZodType } from "./index.ts";
import { util } from "./helpers/index.ts";
import { Primitive } from "./helpers/typeAliases.ts";
import { ZodParsedType } from "./helpers/util.ts";
@@ -365,3 +365,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
@@ -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");
// });
3 changes: 3 additions & 0 deletions deno/lib/__tests__/firstparty.test.ts
Original file line number Diff line number Diff line change
@@ -84,6 +84,9 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodReadonly:
break;
case z.ZodFirstPartyTypeKind.ZodTemplateLiteral:
break;

default:
util.assertNever(def);
}
Loading

0 comments on commit 5b532d8

Please sign in to comment.