Skip to main content

Overview

The Guard module is heavily inspired by Zod; that’s not a coincidence, and it’s worth saying plainly. Zod’s chainable schema builders, type inference from schemas, and developer experience set a high bar for validation libraries in the TypeScript ecosystem. Guard borrows those core ideas directly. The differences are intentional design choices driven by how Guard fits into the broader ts-chas ecosystem, not attempts to replace Zod entirely. If your team already knows and loves Zod, that’s a great reason to keep using it. If you’re evaluating Guard, this page explains what’s the same and what’s different.

What they share

Both Guard and Zod:
  • Define schemas with a chainable, declarative API
  • Infer TypeScript types from schemas via utility types (InferGuard / z.infer)
  • Implement Standard Schema v1 via the ~standard property, so both work with tRPC, react-hook-form, Drizzle, and any other Standard Schema-compatible library
  • Validate strings, numbers, booleans, objects, arrays, tuples, unions, intersections, enums, and literals
  • Support custom validators and refinements
  • Handle null, undefined, and optional fields

Key differences

In Zod, you call .parse() or .safeParse() to validate a value. The schema and the type predicate are separate concerns.In ts-chas, every guard is the type predicate. You can call it directly as a function, and TypeScript narrows the type at the call site. You most likely don’t need a separate parse step for simple checks.
// Zod
if (z.string().email().safeParse(value).success) {
  // value is still typed as unknown here — Zod doesn't narrow in-place
}

// ts-chas: the guard itself is the type predicate (slightly less boilerplate)
import { is } from 'ts-chas/guard';

if (is.string.email(value)) {
  // value is narrowed to string here
  sendEmail(value); // TypeScript knows it's a string
}
This also works inline in type guard function signatures:
function processEmail(v: unknown): v is string {
  return is.string.email(v);
}
You can use guards anywhere TypeScript expects a type predicate: in if statements, Array.prototype.filter, assertion functions, or as standalone validators.
ts-chas includes cryptographic hash validation via @noble/hashes, a well-audited, zero-dependency cryptography library. The implementation is isomorphic: it works identically in browser, Node.js, and edge runtimes like Cloudflare Workers and Deno, with no polyfills required.
import { is } from 'ts-chas/guard';

// Validate that a string is a valid SHA-256 hex hash
const isSha256 = is.string.hash({ alg: 'sha256', enc: 'hex' });
isSha256('a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'); // true

// Verify a hash against plaintext (timing-safe comparison)
const isValidHash = is.string.hash({ alg: 'sha256' }).verify('mypassword');
isValidHash(someHashFromDb); // true or false, comparison is timing-safe

// Supported algorithms: sha1, sha256, sha384, sha512, md5
// Supported encodings:  hex, base64, base64url
Zod has no built-in hash validation. You would need to implement this yourself using a separate library.
When you call .parse() on a guard in ts-chas, it returns a Result<T, GuardErr> , not a Zod-style { success, data, error } object. This means you can chain directly using the full Result API:
import { is } from 'ts-chas/guard';

const result = is.string.email.parse(input); // Result<string, GuardErr>

result.map(email => sendWelcome(email));     // chain directly, no unwrapping needed
result.mapErr(err => logValidationError(err));
Guard .parse() is fail-fast: it returns on the first error. For collecting all errors across a nested structure, use defineSchemas (see below).
When you define a schema with defineSchemas and call .parse(), you get Result<T, AggregateGuardError> : a class with an array of structured errors, one per failed field. Each error carries the field path, the expected type, and a human-readable message.
import { is, defineSchemas } from 'ts-chas/guard';

const schemas = defineSchemas({
  User: is.object({
    name: is.string.min(1),
    age: is.number.gt(0),
    email: is.string.email,
  }),
});

const result = schemas.User.parse({ name: '', age: -1, email: 'not-an-email' });

if (result.isErr()) {
  for (const e of result.error.errors) {
    console.log(`${e.path.join('.')}: ${e.message}`);
    // User.name: Value "" failed validation
    // User.age: Value -1 failed validation
    // User.email: Value "not-an-email" failed validation
  }
}
This is comparable to Zod’s .safeParse() returning error.issues, but ts-chas returns the errors as a typed array in a Result, rather than inside a discriminated union object.
Guards are designed to compose with the rest of ts-chas — Result, Task, and Option — without any adapter layer:
import { is } from 'ts-chas/guard';
import { optionFromGuard } from 'ts-chas/option';

// Convert a guard check directly into an Option
const maybeEmail = optionFromGuard(rawInput, is.string.email);
// Some(rawInput) if it's a valid email, None otherwise

// Wrap a function so its inputs and output are validated at runtime
const processAge = is
  .function({ input: [is.number.gte(18)], output: is.string })
  .implResult(age => `You are ${age} years old`);

processAge(20); // Result.Ok('You are 20 years old')
processAge(15); // Result.Err(GuardErr)
This is unique to ts-chas.
You can extend the is namespace with your own guards while retaining full access to the built-in guards:
import { is } from 'ts-chas/guard';

const myIs = is.extend({
  positiveEmail: (v: unknown): v is string =>
    is.string.email(v) && !v.includes('+'),
});

myIs.positiveEmail('user@example.com'); // true
myIs.positiveEmail('user+tag@example.com'); // false
myIs.string('still works');              // true — all built-ins are preserved
This is similar in spirit to Zod’s custom refinements, but is.extend creates a new is instance with a fully extended namespace, so you can share myIs across your codebase and call your custom guards exactly like built-in ones.

When to use each

Use Zod if:
  • You’re in a Zod-centric codebase (tRPC v10 with Zod, Prisma Zod generators, etc.)
  • Your team already knows Zod well and the switching cost isn’t worth the gain
  • You need features Guard doesn’t yet have
Use ts-chas/guard if:
  • You want inline type predicate usage without a separate parse step
  • You need isomorphic hash validation without a third-party dependency
  • You want tight integration with Result, Task, and Option
  • You’re building a new codebase and want a cohesive ecosystem
Both libraries implement Standard Schema v1, so migrating guards/schemas between them for form libraries (react-hook-form, TanStack Form) or tRPC is straightforward — swap the import and adjust the API calls.
The Guard module is actively inspired by Zod’s excellent design. Many of the patterns you already know from Zod carry over directly. The two libraries are more complementary than competing: if you’re evaluating ts-chas/guard, you’ll find the mental model familiar, and the differences are additive rather than breaking.