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.

The is object

Import is from ts-chas/guard. It is the single entry point for all built-in guards.
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:

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.
Every chain step returns a new, independent guard. Storing intermediate guards is safe:

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.

.assert(value)

Like .parse(), but throws a GuardErr on failure instead of returning an Err. Returns the typed value on success.

.error(msg) / .error(fn)

Overrides the default error message. Accepts a static string or a function receiving { meta, value }.

.nullable

Widens the guard to also accept null. Returns Guard<T | null>.

.optional

Widens the guard to also accept undefined. Returns Guard<T | undefined>.

.nullish

Widens the guard to also accept null or undefined. Returns Guard<T | null | undefined>.

.and(otherGuard)

Logical AND: both guards must pass. The value is typed as T & U.

.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.

.where(predicate)

Adds a custom inline validation rule. The predicate receives the (possibly transformed) value.

.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.

.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.

.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.

.refine(fn)

Like .transform(), but the output type stays T and type-specific helpers are preserved.

.not

Inverts the guard. Passes when the original fails; typed as Guard<unknown>.

.array

Wraps the guard as an element guard for arrays. Equivalent to is.array(thisGuard).

.coerce

Adds coercion support to the guard. When enabled, the guard attempts to cast “loose” inputs (like numeric strings or truthy values) into the target type before validation.
Coercion happens during .parse(), .assert(), and Standard Schema validation.

.arbitrary()

Returns a Promise that resolves to a fast-check Arbitrary<T> for the guard. Requires fast-check to be installed (npm install fast-check). The arbitrary reflects all constraints accumulated through the helper chain.

.generate(n?)

Generates n valid values that satisfy this guard (default: 1, returns a single value). Requires fast-check to be installed (npm install fast-check). Generated values are guaranteed to pass the guard’s predicate.

.toJsonSchema()

Serializes the guard to a JSON Schema Draft-07 compatible object. Captures constraints accumulated through the helper chain (min/max/email/etc.), recursively resolves object shapes and array element types, and handles nullable/optional variants.
Best-effort: exotic guards (lazy, custom functions) fall back to {}.

.whereAsync(predicate)

Appends an async predicate check, switching to async mode. The returned AsyncGuard<T> has .parseAsync() returning a ResultAsync<T, GuardErr> with the full monadic API.

.refineAsync(fn)

Appends an async same-type transformation, switching to async mode. The resolved value replaces the current value and is passed to subsequent steps.

.transformAsync(fn)

Appends an async type-changing transformation, switching to async mode.

Type coercion

.coerce is available on guards for the core coercible types: string, number, boolean, date, bigint, object, array, and result. It adds an automatic conversion step that runs before validation whenever .parse(), .assert(), or Standard Schema validation is called. If the value already satisfies the guard without coercion, it passes through unchanged. If coercion produces a value that still fails the guard, the result is an ordinary validation error.
Constraint helpers chained after .coerce run against the coerced value:
Coercion does not run in predicate mode (guard(v)). Calling a .coerce guard as a plain predicate returns true if the value is coercible — but the runtime value is not converted. Use .parse() or .assert() to get the transformed value. Nested guards compose naturally: if an object’s fields also use .coerce, inner coercions run after the outer one resolves the object:
For the full coercion rules per type, see the coerce guide.

Test data generation

.generate() and .arbitrary() let you produce valid test data from the same guard that validates it. No separate factory functions or schemas to maintain. Both require fast-check as a peer dependency:

.generate(n?)

Returns a single valid value (Promise<T>) or an array of n valid values (Promise<T[]>). Every generated value is guaranteed to pass the guard’s predicate.
A common testing pattern is generating a batch with .generate(n) and feeding it to it.each:

.arbitrary()

Returns a Promise<Arbitrary<T>> — a fully configured fast-check Arbitrary. Use it for property-based tests with fc.assert and fc.property, or compose it with .map(), .filter(), and .chain():
The generator reads from the guard’s accumulated constraint metadata, so chained helpers are reflected automatically: is.number.int.positive.lte(100) produces integers in [1, 100]. Guards that cannot be mapped to a specific arbitrary fall back to fc.anything(). For the full constraint coverage reference, see the generate and arbitrary guide.

Async validation

When a validation step requires I/O, use .whereAsync(), .refineAsync(), or .transformAsync() to switch a guard into async mode. These return an AsyncGuard<T>, which is not a Guard — it does not have .parse() or the helper chain. Its only output is .parseAsync().

Entering async mode

Any of the three methods can start the async chain. Sync helpers added to the base guard before the async call run first:

.whereAsync(fn) — async predicate

Resolving false fails validation. Subsequent steps are not called if a predicate fails.

.refineAsync(fn) — async same-type transform

The resolved value replaces the current value. The type stays T.

.transformAsync(fn) — async type-changing transform

The return type of fn becomes the new type of the AsyncGuard<U>.

.parseAsync(value, errMsg?)

Runs the full chain and returns ResultAsync<T, GuardErr>. The sync guard runs first; async steps are skipped entirely on sync failure.
ResultAsync exposes the full monadic API (.map(), .andThen(), .match(), .unwrap()):
Steps chain in declaration order and each step receives the value produced by the previous one. For a full reference, see the async guard guide.

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.

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.
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.
Another easy way to infer the type of a guard is by using its .$infer property: