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 { array, check, email, max, min, number, object, parse, pipe, string, validate } from '@sx3/gate';
const UserSchema = 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(UserSchema)({
name: 'Alice',
email: 'alice@example.com',
age: 30,
tags: ['admin'],
});
const result = validate(UserSchema)(input); // Return StandardSchema Result
if (check(UserSchema)(input)) { /* ok */ }Three validation modes. They differ in speed and error detail.
import { parse } from '@sx3/gate';
try { parse(User)(input); }
catch (error) { /* GateError */ }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 β 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) / ! DANGER: don't try make runtime string literal (RCE risk)
pipe(number, refine('$ % 2 === 0', 'Must be even'));import {
array,
bigint,
boolean,
instance,
int,
int8,
int16,
int32,
int64,
literal,
never,
number,
object,
record,
string,
tuple,
uint8,
uint16,
uint32,
uint64,
union,
unknown,
} from '@sx3/gate';string; // typeof === "string"
number; // typeof === "number"
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, nullish, optional } 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.
min(string, 3); // .length >= 3
max(string, 100); // .length <= 100
clamp(string, 0, 100); // .length >= 0 && .length <= 100
min(number, 0); // >= 0
max(number, 100); // <= 100
clamp(number, 0, 100); // >= 0 && <= 100
length(string, 10); // .length === 10
email; // email (regex)
uuid; // UUID v4
url; // http(s)://...
cuid; // CUID
datetime; // ISO 8601 UTC
trim(string); // 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 { max, min, number, pipe, string, to } from '@sx3/gate';
const Username = pipe(string, min(3), max(20));
const NumericId = pipe(string, to(number)); // string β numberimport { merge, object } from '@sx3/gate';
const A = object({ id: number });
const B = object({ name: string });
const C = object({ age: number });
const ABC = merge(A, B, C); // { id: number; name: string; age: number }import { object, strict } from '@sx3/gate';
const StrictUser = strict(object({ id: number, name: string }), true); // strict(schema, deep)
parse(StrictUser)({ id: 1, name: 'a', extra: true });
// β GateError: Unexpected key "extra"Compiles to inline coercion operations β no function calls at runtime.
import { boolean, number, object, pipe, string, to } from '@sx3/gate';
// string β number
to(string, number); // "42" β 42
to(number, string); // 42 β "42"
// string β boolean
to(string, boolean); // "true" β true, "false" β false
to(boolean, string); // true β "true"
// string β bigint
to(string, bigint); // "9007199254740991" β 9007199254740991n
to(bigint, string); // 9007199254740991n β "9007199254740991"
// string β object (JSON.parse / JSON.stringify)
to(string, object({})); // '{"a":1}' β { a: 1 } β uses JSON.parse
to(object({}), string); // { a: 1 } β '{"a":1}' β uses JSON.stringify
to(string, array(number)); // '[1,2]' β [1, 2] β JSON.parse
to(array(number), string); // [1,2] β '[1,2]' β JSON.stringifyCoercion only works between compatible type pairs. Unsupported combinations throw at schema compilation time.
import { string, transform } from '@sx3/gate';
transform(string, s => s.toUpperCase())('hello'); // "HELLO"
transform(string, JSON.parse); // equivalent to to(object({}))import type { Output } from '@sx3/gate';
import { number, object, string } 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