A tiny, fast runtime validation library for TypeScript. Schemas compile to
straight-line JavaScript at first use — no interpretation overhead, no
intermediate objects, just typeof checks and property access.
Sometimes you don't need a full ORM — a raw database client (like Bun's built-in SQLite) is enough. But going raw means losing type safety. TypeScript casts don't verify anything at runtime, and validation libraries like Zod have measurable interpreter overhead on every parse call.
jitype closes that gap. Define a schema once, compile it once, and get a
validator that runs as fast as hand-written typeof checks. Zero dependencies,
~600 lines, and full TypeScript inference.
bun add jitypeimport { obj, str, int, bool, arr, parse, type Infer } from "jitype";
const User = obj({
id: int,
name: str,
email: str,
active: bool,
tags: arr(str),
});
type User = Infer<typeof User>;
// { id: number; name: string; email: string; active: boolean; tags: string[] }
const user = parse(User, data); // throws on invalid input, returns typed valueCompared to Zod v4 on Apple M4 Pro (Bun 1.3.8):
| Benchmark | jitype | Zod | Speedup |
|---|---|---|---|
| Flat object (4 fields) | 0.06 ns | 40.03 ns | ~625x |
| Nested object | 1.12 ns | 102.00 ns | ~91x |
| Wide object (10 fields) | 0.13 ns | 94.75 ns | ~710x |
| Array of 100 objects | 168 ns | 3,550 ns | ~21x |
| Maybe fields | 4.50 ns | 44.87 ns | ~10x |
| Tuple (3 elements) | 2.88 ns | 64.51 ns | ~22x |
| Enum/oneOf | 1.75 ns | 43.60 ns | ~25x |
| Union (string) | 1.73 ns | 21.80 ns | ~13x |
| Union (number) | 1.80 ns | 31.52 ns | ~18x |
Run bun bench.ts to reproduce.
Schemas are plain descriptor objects — they don't do anything on their own.
When you call compile() (or parse(), which calls it internally), the library
walks the schema tree and generates a JavaScript function body using new Function(). The result is a validator with zero interpretation overhead:
// This schema:
const s = obj({ name: str, age: int });
// Compiles roughly to:
function(input) {
if (typeof input !== 'object' || input === null || Array.isArray(input))
throw 'expected object';
if (typeof input["name"] !== 'string')
throw 'name: expected string';
if (typeof input["age"] !== 'number' || !Number.isInteger(input["age"]))
throw 'age: expected integer';
return input;
}Compiled validators are cached (via WeakMap) so the codegen cost is paid only
once per schema.
| Schema | Validates | TypeScript type |
|---|---|---|
str |
typeof === 'string' |
string |
num |
typeof === 'number' and Number.isFinite |
number |
int |
typeof === 'number' and Number.isInteger |
number |
bool |
typeof === 'boolean' |
boolean |
bigint |
typeof === 'bigint' |
bigint |
bytes |
instanceof Uint8Array |
Uint8Array |
nil |
=== null |
null |
undef |
=== undefined |
undefined |
any |
passthrough (no validation) | unknown |
literal("active") // Schema<"active">
literal(42) // Schema<42>
literal(true) // Schema<true>
literal(null) // Schema<null>
oneOf("a", "b", "c") // Schema<"a" | "b" | "c">
oneOf(1, 2, 3) // Schema<1 | 2 | 3>arr(str) // Schema<string[]>
obj({ name: str, age: int }) // Schema<{ name: string; age: number }>
tuple(str, int, bool) // Schema<[string, number, boolean]>
record(num) // Schema<Record<string, number>>maybe(str) // Schema<string | undefined> — accepts null or undefined, normalizes to undefined
withDefault(str, "anon") // Schema<string> — replaces null/undefined with defaultconst User = obj({ id: int, name: str, email: str });
extend(User, { role: str }) // { id, name, email, role }
pick(User, "id", "name") // { id, name }
omit(User, "id") // { name, email }
partial(User) // { id?, name?, email? }
// Compose them:
partial(omit(User, "id")) // { name?, email? } — useful for update payloads// Union — tries schemas in order. Optimizes to a typeof switch for primitives,
// a discriminant switch for tagged objects, or falls back to try/catch.
union(str, num, bool)
// Discriminated object unions are auto-detected:
union(
obj({ type: literal("a"), val: str }),
obj({ type: literal("b"), val: num }),
)
// Compiles to: switch(input.type) { case "a": ...; case "b": ... }
// Lazy — for recursive types
type Tree = { value: number; children: Tree[] };
const tree: Schema<Tree> = obj({
value: num,
children: arr(lazy(() => tree)),
});
// Refine — custom predicates
const positive = refine(num, (n) => n > 0, "expected positive number");
const email = refine(str, (s) => s.includes("@"), "expected email");
// Chained refinements
const evenPositive = refine(
refine(int, (n) => n > 0, "expected positive"),
(n) => n % 2 === 0,
"expected even",
);// Preprocess — coerce input before validation
const coercedInt = preprocess(int, (v) => Number(v));
parse(coercedInt, "42"); // 42
const trimmed = preprocess(
refine(str, (s) => s.length > 0, "required"),
(v) => typeof v === "string" ? v.trim() : v,
);
// Transform — change the value after validation
const upper = transform(str, (s) => s.toUpperCase());
parse(upper, "hello"); // "HELLO"
// JSON — validate a JSON string against a schema
const payload = json(obj({ id: int }));
parse(payload, '{"id":1}'); // validates, returns original string// compile() returns a reusable validator function (cached per schema object)
const validate = compile(User);
validate(data); // throws on invalid, returns typed value
// parse() is shorthand for compile(schema)(input)
parse(User, data);import type { Schema, Infer, InferObject } from "jitype";
type User = Infer<typeof User>;
// Extracts the TypeScript type from any Schema<T>Errors are thrown as strings with dot-path and bracket notation:
expected string
name: expected string
user.profile.age: expected integer
items[2]: expected string
items[1].id: expected integer
data[0].pair[1]: expected integer
[1][b]: expected finite number
import { Database } from "bun:sqlite";
import { obj, str, int, maybe, compile } from "jitype";
const db = new Database("app.db");
const UserRow = obj({
id: int,
name: str,
email: str,
bio: maybe(str),
});
const validateUser = compile(UserRow);
function getUser(id: number) {
const row = db.query("SELECT id, name, email, bio FROM users WHERE id = ?").get(id);
return validateUser(row); // runtime validated + fully typed
}MIT