Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.cbrock.dev/llms.txt

Use this file to discover all available pages before exploring further.

Guards for the core coercible types — string, number, boolean, date, bigint, object, array, and result — expose a .coerce property that adds automatic type conversion before validation. When a value does not satisfy the guard’s predicate, .coerce first converts it to the target type and then re-validates. If the value already passes without coercion, it is returned unchanged.

Basic usage

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

// Without coercion: fails
is.number.parse('42').isOk(); // false

// With coercion: parses '42' → 42, then validates
is.number.coerce.parse('42').unwrap(); // 42
.coerce slots naturally into the chain. Any helpers added after it constrain the coerced value:
is.number.coerce.gt(10).multipleOf(2).parse('12').unwrap(); // 12
is.number.coerce.gt(10).multipleOf(2).parse('8').isOk(); // false

When coercion runs

Coercion takes effect in the operations that return or transform values:
  • .parse(v) — returns Result<T, ChasErr> after coercing and validating
  • .assert(v) — throws if invalid, returns the coerced value if valid
  • Standard Schema validation — used by integrations that call the Standard Schema interface
Coercion does NOT run in predicate (function-call) mode:
const guard = is.number.coerce;
guard('42'); // true  -- the "type lie"; see below
guard.parse('42').unwrap(); // 42   -- actual coercion happens here

The type lie

When you call a .coerce guard as a plain predicate (guard(v)), it returns true if the value is coercible to the target type, but it does not actually perform the conversion. TypeScript’s type narrowing therefore reflects the coerced type in narrowing position even though the runtime value is still the original. This is an intentional design trade-off: predicates must be synchronous and allocation-free, while coercion sometimes allocates (new Date, JSON.parse, etc.). Use .parse() or .assert() whenever you need the converted value.
const guard = is.number.coerce;

// Predicate: reports feasibility only
if (guard('42')) {
	// TypeScript says this is `number`, but at runtime it is still '42'
	// If you need the number, use parse() instead
}

// Parse: performs the actual coercion
const result = guard.parse('42');
if (result.isOk()) {
	const n = result.unwrap(); // 42 as a number
}

Coercion rules by type

is.string.coerce

Input typeResult
numberString(n) — e.g. 123'123'
booleanString(b)true'true', false'false'
Datedate.toISOString()
null'null'
undefined'undefined'
Anything elseString(v) (always succeeds)
String coercion is total: any value can become a string, so is.string.coerce only fails when downstream helpers (.min, .email, etc.) reject the result.
const guard = is.string.coerce;
guard.parse(123).unwrap(); // '123'
guard.parse(true).unwrap(); // 'true'
guard.parse(new Date('2023-01-01T00:00:00Z')).unwrap(); // '2023-01-01T00:00:00.000Z'

is.string.coerce.trim().min(5).parse('  hello  ').unwrap(); // 'hello'
is.string.coerce.trim().min(5).parse(12345).unwrap(); // '12345'

is.number.coerce

Input typeResult
string (trimmable to a valid number)Number(trimmed)
string (empty after trim)0
string (not a number)passes original through (validation fails)
booleantrue1, false0
Datedate.getTime() (milliseconds since epoch)
null / undefinedpasses original through (validation fails)
const guard = is.number.coerce;
guard.parse('42').unwrap(); // 42
guard.parse('12.3').unwrap(); // 12.3
guard.parse('').unwrap(); // 0
guard.parse(true).unwrap(); // 1
guard.parse(false).unwrap(); // 0

guard.parse('abc').isOk(); // false -- not a number, passes through, base guard rejects
guard.parse(null).isOk(); // false

is.boolean.coerce

Boolean coercion uses an explicit list of recognized patterns rather than JavaScript’s built-in truthiness, so strings like 'false', '0', and 'no' correctly become false.
InputResult
'true', '1', 'yes', 'on', 'active', 'enabled'true
'false', '0', 'no', 'off', 'inactive', 'disabled'false
Number 1true
Number 0false
Any other string or numberpasses original through (validation fails)
Other typespasses original through (validation fails)
Matching is case-insensitive and whitespace-trimmed.
const guard = is.boolean.coerce;
guard.parse('true').unwrap(); // true
guard.parse('false').unwrap(); // false
guard.parse('on').unwrap(); // true
guard.parse('off').unwrap(); // false
guard.parse(1).unwrap(); // true
guard.parse(0).unwrap(); // false

guard.parse('maybe').isOk(); // false -- not a recognized pattern
guard.parse('TRUE').unwrap(); // true  -- case-insensitive

is.date.coerce

Input typeResult
ISO 8601 stringnew Date(string)
Numeric timestamp stringnew Date(string) (milliseconds)
numbernew Date(number) (milliseconds since epoch)
Invalid stringpasses original through (validation fails)
Other typespasses original through (validation fails)
const guard = is.date.coerce;
const now = Date.now();
guard.parse(now).unwrap().getTime(); // now

guard.parse('2023-01-01').unwrap().toISOString(); // '2023-01-01T00:00:00.000Z'

// Chain with date helpers -- coercion happens first, then constraint checks
is.date.coerce.after(new Date('2020-01-01')).parse('2023-06-15').isOk(); // true
is.date.coerce.after(new Date('2020-01-01')).parse('2019-01-01').isOk(); // false

is.bigint.coerce

Input typeResult
Integer stringBigInt(string)
Integer numberBigInt(number)
Float string / float numberthrows internally, passes original through
Other typespasses original through (validation fails)
const guard = is.bigint.coerce;
guard.parse('123').unwrap(); // 123n
guard.parse(123).unwrap(); // 123n

guard.parse('12.5').isOk(); // false -- BigInt() rejects non-integers
guard.parse('abc').isOk(); // false

is.object(shape).coerce and is.array(guard).coerce

Both object and array coercion parse JSON strings. The string must begin with { or [ respectively after trimming; any other string is passed through unchanged.
// Object from JSON
is.object({ a: is.number }).coerce.parse('{"a": 123}').unwrap();
// { a: 123 }

// Array from JSON
is.array(is.number).coerce.parse('[1, 2, 3]').unwrap();
// [1, 2, 3]

// Invalid JSON string passes through, validation fails
is.object({ a: is.number }).coerce.parse('not json').isOk(); // false

is.result().coerce

Result coercion revives a plain object ({ ok: true, value: X } or { ok: false, error: E }) back into a fully-featured Result instance, restoring all methods (.map, .mapErr, .unwrap, .unwrapErr, etc.). This is useful when a Result has been serialized to JSON and then deserialized: the round-trip strips the prototype methods, and .coerce restores them.
// Simulate a round-tripped Ok Result
const raw = { ok: true, value: 42 };
const guard = is.result(is.number, is.unknown).coerce;

const outer = guard.parse(raw);
// outer is Result<Result<number, unknown>, ChasErr>
const revived = outer.unwrap();
// revived is a Result<number, unknown> class instance with all methods
revived.isOk(); // true
revived.unwrap(); // 42
typeof revived.map; // 'function'

// Err Results work the same way
const rawErr = { ok: false, error: 'something went wrong' };
const guardErr = is.result(is.unknown, is.string).coerce;
const revivedErr = guardErr.parse(rawErr).unwrap();
revivedErr.isErr(); // true
revivedErr.unwrapErr(); // 'something went wrong'

// Non-objects fall back and fail validation
guard.parse('not an object').isOk(); // false
guard.parse(null).isOk(); // false

Chaining: coerce position matters

.coerce should appear directly after the base guard (before any constraint helpers). Constraint helpers added after .coerce run against the coerced value:
// Correct: coerce first, then constrain
is.number.coerce.gt(0).lte(100).parse('42').unwrap(); // 42

// This also works: coerce and int together
is.number.int.coerce.parse('7').unwrap(); // 7

Nested coercion

Coercion composes across nested guards. If an object’s field guards also use .coerce, the inner coercions run after the outer coercion produces the object:
const guard = is.object({
	a: is.number.coerce,
}).coerce;

// Outer .coerce: string → { a: "123" }
// Inner .coerce: "123" → 123
guard.parse('{"a": "123"}').unwrap();
// { a: 123 }

Failure semantics

If a value cannot be coerced, the coercer returns the original value and lets the base guard reject it normally. There are no special coercion errors: a failed coercion produces the same Result<never, ChasErr> as any other validation failure.
is.number.coerce.parse('abc').isOk(); // false
is.boolean.coerce.parse('maybe').isOk(); // false
is.date.coerce.parse({}).isOk(); // false

Summary

TypeConverts from
stringany value via String(), dates via .toISOString()
numbernumeric strings, booleans, dates
booleanrecognized truthy/falsy string patterns, 1/0
dateISO strings, timestamp numbers
bigintinteger strings and integer numbers
objectJSON strings starting with {
arrayJSON strings starting with [
resultplain { ok, value/error } objects (revives methods)