Skip to content

corvid-agent/guard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

guard

Type-safe schema validation with inference, parsing, and coercion. Zero deps. TypeScript-first.

Part of the corvid-agent zero-dependency TypeScript toolkit.

Install

bun add @corvid-agent/guard
# or
npm install @corvid-agent/guard

Quick Start

import * as g from "@corvid-agent/guard";

// Define a schema
const User = g.object({
  name: g.string().min(1),
  email: g.string().email(),
  age: g.number().int().min(0).optional(),
  role: g.enum(["user", "admin"] as const).default("user"),
});

// Infer the TypeScript type
type User = g.Infer<typeof User>;
// { name: string; email: string; age?: number; role: "user" | "admin" }

// Parse (throws on failure)
const user = User.parse({ name: "Alice", email: "alice@example.com" });

// Safe parse (never throws)
const result = User.safeParse(input);
if (result.ok) {
  console.log(result.value);
} else {
  console.log(result.issues);
}

Schemas

Primitives

g.string()       // string
g.number()       // number (rejects NaN)
g.boolean()      // boolean
g.date()         // Date (rejects invalid dates)

String Checks

g.string().min(3)              // minimum length
g.string().max(100)            // maximum length
g.string().length(5)           // exact length
g.string().email()             // email format
g.string().url()               // URL format
g.string().pattern(/^[a-z]+$/) // regex
g.string().startsWith("http")  // prefix
g.string().endsWith(".ts")     // suffix
g.string().includes("needle")  // substring
g.string().nonempty()          // min(1)

// Transforms
g.string().trim()              // trim whitespace
g.string().toLowerCase()       // to lowercase
g.string().toUpperCase()       // to uppercase

Number Checks

g.number().min(0)         // minimum value
g.number().max(100)       // maximum value
g.number().int()          // integer only
g.number().positive()     // > 0
g.number().negative()     // < 0
g.number().nonnegative()  // >= 0
g.number().finite()       // no Infinity
g.number().multipleOf(5)  // divisible by

Objects

const schema = g.object({
  name: g.string(),
  age: g.number(),
});

// Unknown keys are stripped by default
schema.strict()                        // reject unknown keys
schema.passthrough()                   // keep unknown keys
schema.catchall(g.string())            // validate unknown keys

// Utilities
schema.extend({ email: g.string() })   // add fields
schema.pick("name")                    // select fields
schema.omit("age")                     // remove fields
schema.partial()                       // all optional
schema.merge(otherSchema)              // combine schemas
schema.keyof()                         // enum of keys

Arrays & Tuples

g.array(g.number())                  // number[]
g.array(g.string()).min(1).max(10)   // bounded array
g.array(g.number()).nonempty()       // at least one element

g.tuple(g.string(), g.number())      // [string, number]

Records

g.record(g.number())                              // Record<string, number>
g.record(g.enum(["a", "b"] as const), g.number()) // Record<"a"|"b", number>

Enums & Literals

g.literal("hello")                             // exact value
g.enum(["red", "green", "blue"] as const)      // string union
g.nativeEnum(MyTSEnum)                         // TypeScript enum

Unions & Intersections

g.union(g.string(), g.number())                // string | number
g.intersection(schemaA, schemaB)               // A & B

Special Types

g.any()          // any
g.unknown()      // unknown
g.never()        // never (always fails)
g.null()         // null
g.undefined()    // undefined
g.instanceof(Date)  // instanceof check
g.lazy(() => schema) // recursive schemas

Modifiers

g.string().optional()       // string | undefined
g.string().nullable()       // string | null
g.string().default("hi")    // defaults to "hi" when undefined

Transform & Refine

// Transform the output type
const len = g.string().transform((s) => s.length);
// Schema<number>

// Custom validation
const even = g.number().refine((n) => n % 2 === 0, "Must be even");

// Chain schemas
const parsed = g.string()
  .transform((s) => Number(s))
  .pipe(g.number().int().positive());

Coercion

Parse with automatic type coercion:

g.number().coerce("42")     // 42
g.number().coerce(true)     // 1
g.boolean().coerce("true")  // true
g.boolean().coerce(1)       // true
g.string().coerce(42)       // "42"
g.date().coerce("2024-01-15T00:00:00Z") // Date

Type Guard

const schema = g.string().min(1);

if (schema.is(value)) {
  // value is narrowed to string
}

Error Handling

try {
  schema.parse(badInput);
} catch (err) {
  if (err instanceof g.ValidationError) {
    for (const issue of err.issues) {
      console.log(issue.path);     // ["user", "email"]
      console.log(issue.message);  // "Invalid email address"
      console.log(issue.expected); // "email"
    }
  }
}

Type Inference

const schema = g.object({
  name: g.string(),
  tags: g.array(g.string()),
  config: g.object({
    debug: g.boolean().default(false),
  }).optional(),
});

// Automatically inferred:
type MyType = g.Infer<typeof schema>;
// {
//   name: string;
//   tags: string[];
//   config?: { debug: boolean };
// }

License

MIT

About

Type-safe schema validation with inference, parsing, and coercion. Zero deps. TypeScript-first.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors