Readme

minizod

Tiny Zod implementation.

Why

Zod is a dense library, and its module structure (or lack thereof) makes it difficult for bundlers to tree-shake unused modules.

Additionally, using Zod in vals requires the await import syntax which means having to wrap every schema in a Promise and awaiting it. This is extremely annoying.

So this is a lil-tiny-smol Zod meant for use in vals. A noteworthy use-case is using minizod to generate tyep-safe API calls to run vals outside of Val Town (such as client-side).

Type-safe API call example

We can use minizod to create type safe HTTP handlers and generate the corresponding code to call them using Val Town's API, all in a type-safe manner.

First, create a schema for a function. The following example defines a schema for a function that takes a { name: string } parameter and returns a Promise<{ text: string }>.

const minizodExampleSchema = () =>
  @zackoverflow.minizod().chain((z) =>
    z
      .func()
      .args(z.tuple().item(z.object({ name: z.string() })))
      .ret(z.promise().return(z.object({ text: z.string() })))
  );

With a function schema, you can then create an implementation and export it as a val:

const minizodExample = @me.minizodExampleSchema().impl(async (
  { name },
) => ({ text: `Hello, ${name}!` })).json()

In the above example, we call .impl() on a function schema and pass in a closure which implements the actual body of the function. Here, we simply return a greeting to the name passed in.

We can call this val, and it will automatically parse and validate the args we give it:

// Errors at compile time and runtime for us!
const response = @me.minizodExample({ name: 420 })

Alternatively, we can use the .json() function to use it as a JSON HTTP handler:

const minizodExample = @me.minizodExampleSchema().impl(async (
  { name },
) => ({ text: `Hello, ${name}!` })).json() // <-- this part

We can now call minizodExample through Val Town's API. Since we defined a schema for it, we know exactly the types of its arguments and return, which means we can generate type-safe code to call the API:

let generatedApiCode =
  @zackoverflow.minizodFunctionGenerateTypescript(
    // put your username here
    "zackoverflow",
    "minizodExample",
    // put your auth token here
    "my auth token",
    @me.minizodExampleSchema(),
  );

This generates the following the code:

export const fetchMinizodExample = async (
  ...args: [{ name: string }]
): Promise<Awaited<Promise<{ text: string }>>> =>
  await fetch(`https://api.val.town/v1/run/zackoverflow.minizodExample`, {
    method: "POST",
    body: JSON.stringify({
      args: [...args],
    }),
    headers: {
      Authorization: "Bearer ksafajslfkjal;kjf;laksjl;fajsdf",
    },
  }).then((res) => res.json());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
export function minizod() {
type UnknownInput = unknown | undefined | null;
type ZType =
| ZAny
| ZUnknown
| ZPromise<ZType>
| ZFunction<ZTuple, ZType>
| ZString
| ZTuple<
[
ZType,
...ZType[],
] | []
>
| ZArray<ZType>
| ZObject<Record<string, ZType>>
| ZRecord<ZType>;
type Infer<T extends ZType> = T["type"] extends "unknown" ? unknown
: T["type"] extends "string" ? string
: T extends ZTuple ? InferTuple<T>
: T["type"] extends "object" ? InferObject<T>
: T extends ZRecord<ZType> ? InferRecord<T>
: T["type"] extends "array" ? InferArray<T>
: T["type"] extends "promise" ? InferPromise<T>
: T["type"] extends "any" ? any
: "invalid type";
type InferPromise<T> = T extends ZPromise<infer P extends ZType>
? Promise<Infer<P>>
: never;
type InferArray<T> = T extends ZArray<infer P extends ZType> ? Array<Infer<P>>
: never;
type InferRecord<T> = T extends ZRecord<infer V extends ZType>
? Record<string, Infer<V>>
: never;
type InferObject<T> = T extends
ZObject<infer Obj extends Record<string, ZType>> ? {
[k in keyof Obj]: Infer<Obj[k]>;
}
: never;
type InferFunction<T> = T extends ZFunction<
infer Args extends ZTuple<
[
ZType,
...ZType[],
] | []
>,
infer Ret extends ZType
> ? (...args: InferTuple<Args>) => Infer<Ret>
: never;
type InferForEach<T extends ZType[]> = T extends [] ? [] : T extends [
infer First extends ZType,
] ? [
Infer<First>,
]
: T extends [
infer First extends ZType,
...infer Rest extends ZType[],
] ? [
Infer<First>,
...InferForEach<Rest>,
]
: [];
type InferTuple<
T extends ZTuple<
[] | [
ZType,
...ZType[],
]
>,
> = T extends ZTuple<[]> ? [] : T extends ZTuple<[
infer First extends ZType,
...infer Rest extends ZType[],
]> ? [
Infer<First>,
...InferForEach<Rest>,
]
: [];
class ZRecord<V extends ZType> {
type: "record" = "record";
__vals: V;
constructor(__vals: V) {
this.__vals = __vals;
}
async parse(val: UnknownInput): Promise<Record<string, Infer<V>>> {
if (val === undefined || val === null)
throw new Error("undefined");
for (const key in val) {
if (typeof key !== "string")
throw new Error(`Key ${key} is not a string`);
// @ts-expect-error
await this.__vals.parse(val[key]);
}
return val as any;
}
parseSync(val: UnknownInput): Record<string, Infer<V>> {
if (val === undefined || val === null)
throw new Error("undefined");
for (const key in val) {
if (typeof key !== "string")
throw new Error(`Key ${key} is not a string`);
👆 This is a val. Vals are TypeScript snippets of code, written in the browser and run on our servers. Create scheduled functions, email yourself, and persist small pieces of data — all from the browser.