schema.ts in TypeScript
A tiny schema validator where the parsed type is inferred from the schema.
type Validator<T> = {
readonly _type?: T;
parse(input: unknown, path?: string): T;
};
type Infer<V> = V extends Validator<infer T> ? T : never;
class ValidationError extends Error {
constructor(readonly path: string, message: string) {
super(`${path || "value"}: ${message}`);
this.name = "ValidationError";
}
}
const string = (): Validator<string> => ({
parse(input, path = "") {
if (typeof input !== "string") {
throw new ValidationError(path, "expected a string");
}
return input;
},
});
const number = (): Validator<number> => ({
parse(input, path = "") {
if (typeof input !== "number" || Number.isNaN(input)) {
throw new ValidationError(path, "expected a number");
}
return input;
},
});
const optional = <T>(inner: Validator<T>): Validator<T | undefined> => ({
parse(input, path = "") {
return input === undefined ? undefined : inner.parse(input, path);
},
});
const array = <T>(inner: Validator<T>): Validator<T[]> => ({
parse(input, path = "") {
if (!Array.isArray(input)) {
throw new ValidationError(path, "expected an array");
}
return input.map((item, i) => inner.parse(item, `${path}[${i}]`));
},
});
const object = <Shape extends Record<string, Validator<unknown>>>(
shape: Shape,
): Validator<{ [K in keyof Shape]: Infer<Shape[K]> }> => ({
parse(input, path = "") {
if (typeof input !== "object" || input === null) {
throw new ValidationError(path, "expected an object");
}
const source = input as Record<string, unknown>;
const out = {} as { [K in keyof Shape]: Infer<Shape[K]> };
for (const key of Object.keys(shape) as (keyof Shape & string)[]) {
out[key] = shape[key]!.parse(
source[key],
path ? `${path}.${key}` : key,
) as Infer<Shape[typeof key]>;
}
return out;
},
});
const runSchema = object({
lang: string(),
tpm: number(),
stars: optional(number()),
tags: array(string()),
});
type Run = Infer<typeof runSchema>;
const parsed: Run = runSchema.parse({
lang: "ts",
tpm: 98,
tags: ["types", "generics"],
});
console.log(parsed.lang.toUpperCase(), parsed.tags.length, parsed.stars);
try {
runSchema.parse({ lang: "ts", tpm: "fast", tags: [] });
} catch (error) {
console.log(error instanceof ValidationError, (error as Error).message);
}
How it works
- Each validator carries the type it produces in a phantom field.
Infer<typeof schema>reads that type back out.- One definition serves both the run-time check and the static type.
Keywords and builtins used here
ArrayNumberObjectRunShapeTValidatorarrayascatchclassconstconstructorextendsforifisNaNnevernumberofoptionalreadonlyreturnstringsuperthisthrowtrytypeunknown
The run, in numbers
- Lines
- 87
- Characters to type
- 2169
- Tokens
- 658
- Three-star pace
- 110 tpm
At the three-star pace of 110 tokens a minute, this run takes about 359 seconds.
Step 1 of 1 in Encore, step 19 of 19 in Classes & patterns.