Skip to main content
Guards are chainable, immutable TypeScript type predicates. Every guard is callable as (value: unknown) => value is T, so it narrows types in if blocks. Each property access or method call returns a new guard — nothing is mutated.
import { is } from 'ts-chas/guard';

// Direct type predicate — narrows in if blocks
const input: unknown = '  hello@example.com  ';
if (is.string.trim().email(input)) {
  input; // string
}

// .parse() for Result-based validation
const guard = is.string.trim().email.min(5).max(100);
const result = guard.parse(input);
if (result.isOk()) {
  console.log(result.value); // 'hello@example.com' (trimmed)
} else {
  console.log(result.error.message); // GuardErr
}

// .assert() throws if invalid, otherwise returns the typed value
const email = guard.assert(input); // string — throws GuardErr if invalid

The is object

Import is from ts-chas/guard. It is the single entry point for all built-in guards.
import { is } from 'ts-chas/guard';
Every member of is is itself a guard (or a factory that produces one). You chain helpers by accessing properties or calling methods directly on the guard:
// Property access — no parentheses needed
const positiveInt = is.number.int.positive;

// Method call — takes arguments
const shortEmail = is.string.trim().email.max(50);

// Mixed chain
const apiKey = is.string.trim().min(32).max(64).regex(/^[A-Za-z0-9_-]+$/);

How chaining works

Each step in a chain applies left to right. Transformers (like .trim()) mutate the value flowing through the chain, while validators (like .email) refine it.
// Evaluation order for is.string.trim().toLowerCase().email
// 1. Validate: typeof value === 'string'
// 2. Transform: value = value.trim()
// 3. Transform: value = value.toLowerCase()
// 4. Validate: isEmail(value)

const guard = is.string.trim().toLowerCase().email;
guard.parse('  Contact@Example.COM  ');
// Ok('contact@example.com')
Every chain step returns a new, independent guard. Storing intermediate guards is safe:
const base = is.string.trim().email;
const short = base.max(50);  // new guard
const long  = base.min(10);  // different new guard — base is unchanged

Universal helpers

Every guard — regardless of type — exposes the following methods.

.parse(value)

Validates value and returns Result<T, GuardErr>. If the guard has a transform pipeline (e.g., .trim()), the transformed value is returned on success.
const result = is.number.int.positive.parse(42);
// Ok(42)

const result2 = is.number.int.positive.parse(-5);
// Err(GuardErr { message: 'Value -5 failed validation', ... })

.assert(value)

Like .parse(), but throws a GuardErr on failure instead of returning an Err. Returns the typed value on success.
const age = is.number.int.gte(18).assert(userAge);
// Throws GuardErr if userAge < 18 or is not a safe integer

.error(msg) / .error(fn)

Overrides the default error message. Accepts a static string or a function receiving { meta, value }.
const guard = is.number.gte(18).error('Must be 18 or older');
guard.parse(15);
// Err(GuardErr { message: 'Must be 18 or older', ... })

const dynamic = is.string.min(3).error(({ value }) => `"${value}" is too short`);
dynamic.parse('hi');
// Err(GuardErr { message: '"hi" is too short', ... })

.nullable

Widens the guard to also accept null. Returns Guard<T | null>.
const guard = is.string.email.nullable;
guard(null);     // true — typed as string | null
guard('a@b.co'); // true
guard(undefined); // false

.optional

Widens the guard to also accept undefined. Returns Guard<T | undefined>.
const guard = is.number.int.optional;
guard(undefined); // true
guard(42);        // true
guard(null);      // false

.nullish

Widens the guard to also accept null or undefined. Returns Guard<T | null | undefined>.
const guard = is.string.nullish;
guard(null);      // true
guard(undefined); // true
guard('hello');   // true

.and(otherGuard)

Logical AND: both guards must pass. The value is typed as T & U.
const hasName = is.object({ name: is.string });
const hasAge  = is.object({ age: is.number });
const guard = hasName.and(hasAge);

guard({ name: 'Alice', age: 30 }); // true — typed as { name: string } & { age: number }
guard({ name: 'Alice' });          // false

.or(otherGuard)

Logical OR: either guard can pass. The value is typed as T | U. Type-specific helpers are dropped on the result since the type is now a union.
const guard = is.string.or(is.number);
guard('hello'); // true — typed as string | number
guard(42);      // true
guard(true);    // false

.where(predicate)

Adds a custom inline validation rule. The predicate receives the (possibly transformed) value.
const even = is.number.int.where(n => n % 2 === 0);
even.parse(4); // Ok(4)
even.parse(3); // Err(...)

const noSpaces = is.string.where(s => !s.includes(' '));
noSpaces.parse('hello'); // Ok('hello')
noSpaces.parse('hello world'); // Err(...)

.brand(tag)

Adds a compile-time brand to the output type. Has no runtime effect — use it to distinguish semantically different values of the same primitive type.
const UserId = is.string.uuid().brand('UserId');
type UserId = typeof UserId.$infer; // string & { readonly __brand: 'UserId' }

const id = UserId.assert('550e8400-e29b-41d4-a716-446655440000');
// id is typed as UserId — cannot be used where a plain string is expected without casting

.fallback(value)

Sets a fallback value returned by .parse() and .assert() when validation fails, instead of producing an error. Does not affect the boolean type predicate.
const guard = is.number.int.positive.fallback(1);
guard.parse('not a number'); // Ok(1)
guard.parse(-5);             // Ok(1)
if (guard('hello')) {
  // never reaches here — predicate still returns false
}

.transform(fn)

Applies a type-changing transformation to the validated value. The guard still validates the original input; .parse() / .assert() return the transformed value. Drops type-specific helpers since the output type may differ.
const toLength = is.string.transform(s => s.length);
toLength.parse('hello'); // Ok(5)

const parseNum = is.string.transform(s => Number(s)).where(n => !isNaN(n));
parseNum.parse('42'); // Ok(42)

.refine(fn)

Like .transform(), but the output type stays T and type-specific helpers are preserved.
const guard = is.string.refine(s => s.trim().toLowerCase()).email;
guard.parse('  ALICE@EXAMPLE.COM  '); // Ok('alice@example.com')

.not

Inverts the guard. Passes when the original fails; typed as Guard<unknown>.
const notString = is.string.not;
notString(42);      // true
notString('hello'); // false

.array

Wraps the guard as an element guard for arrays. Equivalent to is.array(thisGuard).
const guard = is.string.email.array;
guard(['a@b.co', 'c@d.co']); // true — typed as string[]
guard(['not-an-email']);      // false

Standard Schema (~standard)

Every guard implements the Standard Schema v1 specification via the ~standard property. This makes guards compatible with tRPC, react-hook-form, Drizzle, and any other library that consumes the spec — no adapter needed.
import { is } from 'ts-chas/guard';

// Directly usable anywhere Standard Schema v1 is accepted
const emailGuard = is.string.trim().email;
emailGuard['~standard'].validate('hello@example.com');
// { value: 'hello@example.com' }

emailGuard['~standard'].validate('not-an-email');
// { issues: [{ message: '...', path: [...] }] }

Extending the is namespace

Use is.extend({ ... }) to add custom guards to a new is instance. The base guards remain available on the returned object.
import { is } from 'ts-chas/guard';

const myIs = is.extend({
  email:   is.string.trim().email.max(255),
  posInt:  is.number.int.positive,
  slug:    is.string.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
});

myIs.email('hello@example.com'); // true
myIs.posInt(42);                  // true
myIs.slug('my-post-title');       // true
myIs.string('still works');       // true — base guards preserved
You can pass any value (not just guards) to extend — it performs a shallow merge with baseIs.

Type utilities

InferGuard<T>

Extracts the validated type from a guard.
import type { InferGuard } from 'ts-chas/guard';

const UserGuard = is.object({ name: is.string, age: is.number });
type User = InferGuard<typeof UserGuard>;
// { name: string; age: number }

type Email = InferGuard<typeof is.string.email>;
// string

Guard<T, H>

The guard interface itself. T is the validated type; H is the type-specific helpers object.
import type { Guard } from 'ts-chas/guard';

function validateAndLog<T>(guard: Guard<T>, value: unknown): T | null {
  const result = guard.parse(value);
  if (result.isOk()) return result.value;
  console.error(result.error.message);
  return null;
}

GuardErr

The error type returned by .parse() on failure.
import type { GuardErr } from 'ts-chas/guard';

const result = is.string.email.parse(123);
if (result.isErr()) {
  const e: GuardErr = result.error;
  e.message;  // 'Expected string, but got number (123)'
  e.expected; // 'string'
  e.actual;   // 'number'
  e.name;     // 'string.email'
  e.path;     // []
}