Skip to main content
A Guard<T> is a TypeScript type predicate; a function (value: unknown) => value is T — with chainable helpers for common validations, schema parsing, and transformation. Unlike most validators, guards work as plain if conditions, so narrowing is free.
import { is } from 'ts-chas/guard';

declare const value: unknown;

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

The is namespace

All built-in guards live on the is object:
import { is } from 'ts-chas/guard';

is.string    // Guard<string>
is.number    // Guard<number>
is.boolean   // Guard<boolean>
is.bigint    // Guard<bigint>
is.symbol    // Guard<symbol>
is.null      // Guard<null>
is.undefined // Guard<undefined>
is.object({...}) // Guard<{ ... }>
is.array(guard)  // Guard<T[]>

Chaining helpers

Under the hood, ts-chas chains evaluations from left to right. When you use the guard as a boolean type predicate:
if (is.string.trim().email(value)) {
// ...
}
Here is exactly what happens during the if check:
  1. It validates that value is a string (from is.string).
  2. It applies the .trim() inline to a temporary, internal value.
  3. It passes that temporary, trimmed string into the .email(...) validation rule.
If all three steps succeed, the predicate returns true, and TypeScript safely narrows value to a string. It’s important to remember that because TypeScript type predicates (v is string) cannot mutate variables, the value inside the if block is still the original, _un-trimmed _string you started with. It’s simply _guaranteed _to be an email once you trim it! If you want the actual trimmed string value to work with, you map it through .parse(...) or .assert(...) instead of using it as a raw boolean check:
// Safely returns the fully trimmed/transformed email!
const result = is.string.trim().email.parse(value); 

if (result.isOk()) {
console.log(result.value); // "contact@example.com"
}

Universal helpers

Every guard, regardless of type, has these methods:

.parse(value)Result<T, GuardErr>

The primary way to validate data and get a typed result:
const result = is.string.email.parse(rawInput);
// Result<string, GuardErr>

if (result.isOk()) {
  sendEmail(result.value); // string, already validated
}

.error(message) — custom error messages

const validAge = is.number.gte(18).error('Must be 18 or older');
const result = validAge.parse(15);
// Err({ message: 'Must be 18 or older', ... })

// Optional context to generate dynamic error messages
const validEmail = is.string.email.error(({ meta, value }) => `${meta.name}: expected email, got ${JSON.stringify(value}`);

.nullable(), .optional(), .nullish()

is.string.nullable  // Guard<string | null>
is.string.optional  // Guard<string | undefined>
is.string.nullish   // Guard<string | null | undefined>

.and(), .or()

const stringOrNumber = is.string.or(is.number);  // Guard<string | number>
const richObject = is.object({ name: is.string })
  .and(is.object({ age: is.number }));            // Guard<{ name: string } & { age: number }>

.where(predicate) — arbitrary refinement

const evenNumber = is.number.int.where((n) => n % 2 === 0);

.brand(tag) — branded types

const UserId = is.string.uuid('v4').brand('UserId');
type UserId = typeof UserId.$infer; // string & { __brand: 'UserId' }

Object guards

const isUser = is.object({
  name: is.string.min(1),
  age: is.number.gte(18),
  email: is.string.email,
});

if (isUser(rawData)) {
  rawData.name; // string
  rawData.age;  // number
}

Array guards

const isTags = is.array(is.string).min(1).max(10);
const isUsers = is.array(isUser);

if (isTags(data.tags)) {
  data.tags; // string[]
}
The .array property also works as a shorthand on any guard:
const isStringArray = is.string.array; // Guard<string[]>

Schema parsing

Use defineSchemas to define reusable schemas that collect all validation errors instead of short-circuiting on the first:
import { defineSchemas, type InferSchema } from 'ts-chas/guard';

const schemas = defineSchemas({
  User: {
    id: is.string.uuid('v4'),
    email: is.string.trim().email,
    age: is.number.gte(18).error('Must be an adult'),
    tags: is.array(is.string).min(1),
  },
});

InferSchema — type inference

type User = InferSchema<typeof schemas.User>;
// { id: string; email: string; age: number; tags: string[] }

// Or use the $infer shorthand:
type User = typeof schemas.User.$infer;

.parse(data)Result<T, GuardErr[]>

const result = schemas.User.parse(incomingData);

if (result.isOk()) {
  saveUser(result.value); // typed as User
} else {
  console.error(result.error.map((e) => e.message));
  // ['Expected string, got number (123)', 'Must be an adult']
}

.assert(data) — throw on failure

import { AggregateGuardError } from 'ts-chas/guard';

try {
  schemas.User.assert(incomingData);
} catch (e) {
  if (e instanceof AggregateGuardError) {
    console.error(e.format());
    // { 'User.email': ['Expected email, got "notanemail"'], ... }
  }
}

Standard Schema v1 compatibility

All guards implement the Standard Schema v1 spec via the ~standard property. This means you can drop them directly into tRPC, react-hook-form, Drizzle, and other compatible ecosystems with no adapter needed:
// tRPC
const procedure = t.procedure.input(isUser);

// react-hook-form
const { register } = useForm({ resolver: standardSchemaResolver(isUser) });

Namespace extensions

Add your own guards to the is namespace with is.extend(). Extensions are typed and chainable:
import { is } from 'ts-chas/guard';

const myIs = is.extend({
  positiveEven: (v: unknown): v is number =>
    is.number.positive(v) && is.number.even(v),
});

myIs.positiveEven(4); // true
myIs.positiveEven(3); // false
Store the extended myIs instance as your project’s canonical import. This lets you share custom guards across your codebase without re-extending everywhere.

Hashing with is.string.hash()

The is.string.hash() helper uses @noble/hashes under the hood, which runs isomorphically in Node.js, browsers, and edge runtimes with no native bindings required:
// Validate a SHA-256 hex hash string
const isSha256Hex = is.string.hash({ alg: 'sha256', enc: 'hex' });
isSha256Hex(someHashString); // true if valid length and format

// Timing-safe verification against known plaintext
const verifyPassword = is.string.hash({ alg: 'sha256', enc: 'hex' }).verify('mysecret');
verifyPassword.parse(storedHash); // Ok(string) if storedHash matches sha256('mysecret')
Supported algorithms: sha1, sha256, sha384, sha512, md5. Supported encodings: hex, base64, base64url.