Skip to content
/ jitype Public

Tiny, fast runtime validation library. Schemas compile to straight-line JavaScript.

Notifications You must be signed in to change notification settings

brielov/jitype

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jitype

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.

Why

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.

Install

bun add jitype

Quick start

import { 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 value

Benchmarks

Compared 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.

How it works

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.

API

Primitives

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

Literals and enums

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>

Containers

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>>

Wrappers

maybe(str)                  // Schema<string | undefined>  — accepts null or undefined, normalizes to undefined
withDefault(str, "anon")    // Schema<string>  — replaces null/undefined with default

Object utilities

const 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

Combinators

// 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",
);

Transforms

// 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

Compiler

// 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);

Type utilities

import type { Schema, Infer, InferObject } from "jitype";

type User = Infer<typeof User>;
// Extracts the TypeScript type from any Schema<T>

Error messages

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

Real-world example: Bun SQLite

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
}

License

MIT

About

Tiny, fast runtime validation library. Schemas compile to straight-line JavaScript.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published