Careeroza
Runtime Validation with a Mini Schema
Why Types Alone Are Not Enough
Types disappear at runtime. Data from HTTP requests, forms, files and environment variables can be anything. Libraries like Zod, Valibot and ArkType solve both problems at once: you write a schema, it validates at runtime, and the TypeScript type is inferred from the schema. Here is a ~40 line version to show how it works.
Example
class ValidationError extends Error {}
interface Schema<T> {
parse(x: unknown, path?: string): T;
optional(): Schema<T | undefined>;
}
type Infer<S> = S extends Schema<infer T> ? T : never;
function schema<T>(check: (x: unknown, path: string) => T): Schema<T> {
return {
parse: (x, path = "value") => check(x, path),
optional: () => schema((x, path) => (x === undefined ? undefined : check(x, path))),
};
}
const fail = (path: string, msg: string): never => { throw new ValidationError(`${path} ${msg}`); };
const z = {
string: (min = 0) => schema((x, p) => (typeof x === "string" && x.length >= min ? x : fail(p, min ? `must be a string of at least ${min} chars` : "must be a string"))),
number: (min = -Infinity) => schema((x, p) => (typeof x === "number" && x >= min ? x : fail(p, `must be a number >= ${min}`))),
enum: <const V extends readonly string[]>(values: V) =>
schema((x, p) => (values.includes(x as string) ? (x as V[number]) : fail(p, `must be one of ${values.join("|")}`))),
array: <T>(item: Schema<T>) =>
schema((x, p) => (Array.isArray(x) ? x.map((v, i) => item.parse(v, `${p}[${i}]`)) : fail(p, "must be an array"))),
object: <S extends Record<string, Schema<unknown>>>(shape: S) =>
schema((x, p) => {
if (typeof x !== "object" || x === null) return fail(p, "must be an object");
const out: Record<string, unknown> = {};
for (const key in shape) out[key] = shape[key].parse((x as Record<string, unknown>)[key], `${p}.${key}`);
return out as { [K in keyof S]: Infer<S[K]> };
}),
};
const ProductSchema = z.object({
name: z.string(3),
price: z.number(1),
category: z.enum(["electronics", "books", "fashion"]),
tags: z.array(z.string()).optional(),
});
type Product = Infer<typeof ProductSchema>; // inferred, never written by hand
const good: Product = ProductSchema.parse(JSON.parse('{"name":"Kettle","price":1299,"category":"electronics","tags":["sale"]}'), "body");
console.log(good.name, good.category, good.tags);
for (const raw of ['{"name":"TV","price":20000,"category":"electronics"}', '{"name":"Kettle","price":0,"category":"toys"}', '{"name":"Book","price":299,"category":"books","tags":[1]}']) {
try { ProductSchema.parse(JSON.parse(raw), "body"); } catch (e) { console.log("Invalid:", (e as Error).message); }
}Output
Kettle electronics [ 'sale' ] Invalid: body.name must be a string of at least 3 chars Invalid: body.price must be a number >= 1 Invalid: body.tags[0] must be a string
The Real Zod
import { z } from "zod";
const ProductSchema = z.object({
name: z.string().min(3),
price: z.number().positive(),
category: z.enum(["electronics", "books", "fashion"]),
tags: z.array(z.string()).optional(),
});
type Product = z.infer<typeof ProductSchema>;
const result = ProductSchema.safeParse(await req.json());
if (!result.success) return Response.json(result.error.flatten(), { status: 400 });
const product: Product = result.data;Tip: Validate at the edges of your system (HTTP handlers, form submits, env loading, message consumers). Inside, trust the types.