Gate β a maximally performant TypeScript schema validation and parsing library. Compiles schemas into pure JavaScript on the fly β zero runtime interpretation.
npm install @sx3/gate
bun add @sx3/gate
pnpm add @sx3/gateimport { parse, validate, check, pipe, string, number, object, array, min, max, email } from '@sx3/gate';
const User = object({
name: pipe(string, min(2), max(50)),
email: pipe(string, email),
age: pipe(number, min(0), max(120)),
tags: array(string),
});
const data = parse(User)({
name: 'Alice',
email: 'alice@example.com',
age: 30,
tags: ['admin'],
});
const result = validate(User)(input);
if (result.issues) {
for (const issue of result.issues) console.log(issue.message, issue.path);
} else {
console.log(result.value);
}
if (check(User)(input)) { /* ok */ }Three validation modes. They differ in speed and error detail.
import { parse } from '@sx3/gate';
try { parse(User)(input) }
catch (error) { /* GateError */ }No allocations on the happy path. On error β throw. Ideal for APIs: invalid input β immediate 400.
import { validate } from '@sx3/gate';
const result = validate(User)(input);
if (result.issues) { /* all errors */ }
else { result.value }Returns all errors at once, never throws. Perfect for forms.
import { check } from '@sx3/gate';
if (check(User)(input)) { /* valid */ }No objects, no throw, no arrays β just true/false. The fastest mode. Ideal for guards and filters.
| Mode | Speed | Error info |
|---|---|---|
parse |
β β β | message + path (first error) |
validate |
β ββ | message + path (all errors) |
check |
β β β | none (boolean only) |
On the first call to
parse(schema)/validate(schema)/check(schema), the schema is compiled vianew Functionand cached. Subsequent calls are just a direct function invocation β zero overhead on schema interpretation.
Every type and constraint accepts a custom message as its last argument. The message can be a string or a function (ctx) => string.
Messages are evaluated at compile time and baked directly into the generated function as a string literal. No runtime string interpolation, no extra allocations on every validation call. If you pass
'Must be a string', the compiled code containsthrow new E("Must be a string", ...). If you use a function like({ n }) => \Min ${n}`, the function runs once during compilation and the resulting string"Min 3"` is inlined. The function itself is never called at validation time.
string('Must be a string');
number('Expected a number');
boolean('Must be a boolean');
literal(42, 'Must be exactly 42');
array(string, 'Must be an array of strings');
object({ name: string }, 'Must be an object');pipe(string, min(3, 'At least 3 characters'));
pipe(string, max(100, 'Too long'));
pipe(string, length(10, 'Exactly 10 characters'));
pipe(string, email('Invalid email'));
pipe(number, clamp(0, 100, 'Out of range'));pipe(string, min(3, ({ n }) => `At least ${n} characters`));
pipe(number, clamp(0, 100, ({ min, max }) => `Must be ${min}..${max}`));import { refine } from '@sx3/gate';
// predicate function
pipe(number, refine(n => n % 2 === 0, 'Must be even'));
// inline condition ($ β variable name)
pipe(number, refine('$ % 2 === 0', 'Must be even'));import {
string, number, boolean, bigint,
unknown, never, literal,
object, array, tuple, record, union, instance,
int, int8, int16, int32, int64,
uint8, uint16, uint32, uint64,
} from '@sx3/gate';string // typeof === "string"
number // typeof === "number", not NaN
boolean // typeof === "boolean"
bigint // typeof === "bigint"
unknown // passes anything
never // rejects everything
literal(42) // === 42object({ name: string, age: number }) // { name: string; age: number }
array(string) // string[]
tuple([string, number]) // [string, number]
record(string, number) // Record<string, number>
union([string, number]) // string | number
instance(Date) // instanceof Dateimport { nullable, optional, nullish } from '@sx3/gate';
nullable(string) // string | null
optional(string) // string | undefined
nullish(string) // string | null | undefinedApplied via pipe. For strings/arrays they check .length, for numbers they check the value.
import { min, max, length, clamp, email, uuid, url, cuid, datetime, trim, port } from '@sx3/gate';
pipe(string, min(3)) // .length >= 3
pipe(string, max(100)) // .length <= 100
pipe(string, length(10)) // .length === 10
pipe(number, min(0)) // >= 0
pipe(number, max(100)) // <= 100
pipe(number, clamp(0, 100)) // >= 0 && <= 100
pipe(string, email) // email (regex)
pipe(string, uuid) // UUID v4
pipe(string, url) // http(s)://...
pipe(string, cuid) // CUID
pipe(string, datetime) // ISO 8601 UTC
pipe(string, trim) // whitespace trimpattern is a standalone string schema with a regex check, not a constraint:
import { pattern } from '@sx3/gate';
const HexColor = pattern(/^#[0-9a-f]{6}$/i);
const Digits = pattern(/^\d+$/, 'Digits only');Left-to-right: each function takes a schema and returns a schema.
import { pipe, string, number, min, max, to } from '@sx3/gate';
const Username = pipe(string, min(3), max(20));
const NumericId = pipe(string, to(number)); // string β numberimport { object, merge } from '@sx3/gate';
const A = object({ id: number });
const B = object({ name: string });
const AB = merge(A, B); // { id: number; name: string }import { object, strict } from '@sx3/gate';
const StrictUser = strict(object({ id: number, name: string }));
parse(StrictUser)({ id: 1, name: 'a', extra: true });
// β GateError: Unexpected key "extra"Compiles to inline coercion operations β no function calls at runtime.
import { pipe, string, number, boolean, object, to } from '@sx3/gate';
// string β number
pipe(string, to(number)) // "42" β 42
pipe(number, to(string)) // 42 β "42"
// string β boolean
pipe(string, to(boolean)) // "true" β true, "false" β false
pipe(boolean, to(string)) // true β "true"
// string β bigint
pipe(string, to(bigint)) // "9007199254740991" β 9007199254740991n
pipe(bigint, to(string)) // 9007199254740991n β "9007199254740991"
// string β object (JSON.parse / JSON.stringify)
pipe(string, to(object({}))) // '{"a":1}' β { a: 1 } β uses JSON.parse
pipe(object({}), to(string)) // { a: 1 } β '{"a":1}' β uses JSON.stringify
pipe(string, to(array(number))) // '[1,2]' β [1, 2] β JSON.parse
pipe(array(number), to(string)) // [1,2] β '[1,2]' β JSON.stringifyCoercion only works between compatible type pairs. Unsupported combinations throw at schema compilation time.
import { pipe, string, transform } from '@sx3/gate';
pipe(string, transform(s => s.trim().toUpperCase()));
pipe(string, transform(JSON.parse)); // equivalent to to(object({}))import type { Output } from '@sx3/gate';
import { object, string, number } from '@sx3/gate';
const UserSchema = object({ id: number, name: string });
type User = Output<typeof UserSchema>;
// { id: number; name: string }Works with tRPC, Hono, TanStack Form out of the box:
const schema = object({ name: string });
const result = await schema['~standard'].validate(input);By default ~standard.validate uses parse mode. Switch it:
import { settings } from '@sx3/gate';
settings({ standardMode: 'validate' }); // use validate
settings({ standardMode: 'check' }); // use checkimport { settings } from '@sx3/gate';
settings({
strict: true, // all object() are strict by default
checkNaN: false, // don't check NaN for number()
standardMode: 'parse', // 'parse' | 'validate' | 'check'
});- β‘ JIT compilation β
new Function+ schema-level cache, second call is pure JS - π― Full type inference β
Output<typeof schema>, no manual generics - π¦ Tree-shakeable ESM β take only what you use
- π« Zero dependencies β no runtime dependencies
- π Standard Schema v1 β tRPC, Hono, TanStack Form
MIT Β© SX3