TypeScript-first schema validation library with static type inference.
ts-fortress is a runtime validation library similar to io-ts and Zod, designed to provide type-safe schema validation with excellent TypeScript integration and static type inference.
fill() functionResult<T, readonly ValidationError[]>npm install ts-fortress
yarn add ts-fortress
pnpm add ts-fortress
import { expectType } from 'ts-data-forge';
import * as t from 'ts-fortress';
// Define a schema
const User = t.record({
id: t.string(),
name: t.string(),
age: t.number(),
email: t.optional(t.string()),
isActive: t.boolean(),
});
// Infer TypeScript type
type User = t.TypeOf<typeof User>;
expectType<
User,
Readonly<{
id: string;
name: string;
age: number;
email?: string;
isActive: boolean;
}>
>('=');
// Validate data
const userData = {
id: '123',
name: 'John Doe',
age: 30,
email: 'john@example.com',
isActive: true,
} as const as unknown;
assert.isTrue(User.is(userData));
if (User.is(userData)) {
// userData is now typed as User
userData satisfies User;
expectType<typeof userData.age, number>('=');
expectType<typeof userData.email, string | undefined>('=');
assert.strictEqual(
`User: ${userData.name}, Age: ${userData.age}`,
'User: John Doe, Age: 30',
);
}
// Get validation result with error details
const result = User.validate(userData);
if (t.Result.isOk(result)) {
result.value satisfies User; // typed as User
} else {
console.error(
'Validation errors:',
result.value satisfies readonly t.ValidationError[],
);
}
One of the key design decisions in ts-fortress is that all schema types have explicit default values, which allows for powerful data entry capabilities:
import * as t from 'ts-fortress';
// Every type requires a default value
const UserProfile = t.record({
name: t.string('Anonymous'), // Default: 'Anonymous'
age: t.number(), // Default: 0
email: t.optional(t.string()), // Optional field with default ''
preferences: t.record({
theme: t.string('light'), // Default: 'light'
notifications: t.boolean(true), // Default: true
}),
tags: t.array(t.string()), // Default: empty array []
});
// The fill() function automatically provides missing values
const partialData = {
name: 'John Doe',
preferences: {
theme: 'dark',
// notifications missing - will be filled with default
},
// age, email, tags missing - will be filled with defaults
} as const;
const filledData = UserProfile.fill(partialData);
assert.deepStrictEqual(filledData, {
name: 'John Doe',
age: 0, // β Filled with default
email: '', // β Filled with default
preferences: {
theme: 'dark',
notifications: true, // β Filled with default
},
tags: [], // β Filled with default
});
// fill() is type-safe and always returns a complete object
type UserProfile = t.TypeOf<typeof UserProfile>;
// Important: Default value filling only occurs when fill() is called
// The is() and validate() functions can still detect missing keys
assert.isFalse(UserProfile.is(partialData)); // missing required keys
const result = UserProfile.validate(partialData);
assert.isTrue(t.Result.isErr(result));
assert.deepStrictEqual(
t.validationErrorsToMessages(
result.value satisfies readonly t.ValidationError[],
),
[
'Error at age: missing required key "age".',
'Error at preferences.notifications: missing required key "notifications".',
'Error at tags: missing required key "tags".',
],
);
Most ts-fortress types provide sensible defaults automatically, so you rarely need to specify explicit default values:
import * as t from 'ts-fortress';
// Most common types have built-in defaults
const Schema = t.record({
name: t.string(), // defaults to ""
age: t.number(), // defaults to 0
active: t.boolean(), // defaults to false
tags: t.array(t.string()), // defaults to []
config: t.record({
debug: t.nullable(t.boolean()), // defaults to false
}), // defaults to { debug: false }
});
You only need to specify explicit default values in two cases: when you want custom values, or when using intersection types:
import * as t from 'ts-fortress';
// Custom default values
const ServerConfig = t.record({
port: t.number(3000), // custom default: 3000
host: t.string('localhost'), // custom default: 'localhost'
retries: t.number(5), // custom default: 5
});
assert.deepStrictEqual(ServerConfig.defaultValue, {
port: 3000,
host: 'localhost',
retries: 5,
} satisfies t.TypeOf<typeof ServerConfig>);
// Enum types have built-in defaults
const JobStatus = t.enumType(['started', 'scheduled', 'succeeded', 'failed']); // default: "started"
const JobFulfilledStatus = t.enumType(['succeeded', 'failed', 'cancelled']); // default: "succeeded"
// Intersection types require explicit defaults
const ReportStatus = t.intersection(
[JobStatus, JobFulfilledStatus],
t.enumType(['succeeded', 'failed']), // must provide combined default
);
This is because intersection types can be created from arbitrary types, making it impossible to automatically determine appropriate default values. However, when all constituent types are record types, you can use the mergeRecords function to avoid specifying defaults:
import * as t from 'ts-fortress';
// Using mergeRecords for record-only intersections
const UserWithMetadata = t.mergeRecords([
t.record({
id: t.string(),
name: t.string(),
}),
t.record({
createdAt: t.number(),
updatedAt: t.number(),
}),
// No explicit default needed - automatically combines defaults from both records
]);
assert.deepStrictEqual(UserWithMetadata.defaultValue, {
id: '',
name: '',
createdAt: 0,
updatedAt: 0,
} satisfies t.TypeOf<typeof UserWithMetadata>);
t.string(), t.number(), and t.bigint() accept optional constraint objects that refine both runtime validation and the inferred TypeScript type. Constraints are verified when the schema is createdβinvalid defaults throw immediatelyβand on every is(), validate(), and cast() call.
import * as t from 'ts-fortress';
const Slug = t.string('feature-flag', {
startsWith: 'feature',
includes: '-',
endsWith: 'flag',
nonempty: true,
minLength: 6,
maxLength: 32,
regex: /^[a-z-]+$/u,
});
Slug.is('feature-new-flag'); // true
Slug.is('Feature-Flag'); // false (fails regex)
type SlugType = t.TypeOf<typeof Slug>;
// inferred as a `feature${string}` template literal branded with
// `NonEmptyString`, `MinLengthString<6>` and `MaxLengthString<32>`
String constraints:
startsWith, endsWith, includes β narrow the result to the corresponding template literal typelowercase, uppercasenonempty β brands the result as NonEmptyString (from ts-type-forge)minLength, maxLength β brand the result as MinLengthString<N> / MaxLengthString<N> (from ts-type-forge), which preserve the natural subtyping between length boundsregexminLength / maxLength accept plain numbers, which must be positive integers (>= 1) β passing a non-integer or a value less than 1 throws a TypeError when the type is constructed. Number literals in the range 1..39 are encoded in the result brand (MinLengthString<N> / MaxLengthString<N>); larger literals and dynamically computed numbers are still enforced at runtime, but the result type falls back to plain string to keep the type-level computation cheap.
import * as t from 'ts-fortress';
const Percentage = t.number(100, {
min: 0,
max: 100,
step: 5,
nonNegative: true,
});
Percentage.is(75); // true
Percentage.is(72); // false (fails `step`)
Percentage.is(-5); // false (fails `min`/`nonNegative`)
Numeric constraints cover:
gt, gte, min, lt, lte, maxpositive, nonNegative, negative, nonPositivemultipleOf, stepimport * as t from 'ts-fortress';
const PermissionsMask = t.bigint(0b11_1100n, {
gte: 0n,
lte: (1n << 6n) - 1n,
multipleOf: 1n << 2n,
});
PermissionsMask.is(0b10_1100n); // true
PermissionsMask.is(0b10_1111n); // false (not divisible by 4)
Bigint constraints mirror the numeric API but operate on bigint literals. When multipleOf or step is 0n, only 0n passes the check.
A constrained type carries the constraints it was given on a constraints property, so the numbers a schema validates against can also drive the UI β an <input>'s min / max / step, a character counter β instead of being written down a second time and drifting.
import * as t from 'ts-fortress';
const SignupForm = t.record({
age: t.int(20, { min: 0, max: 120 }),
displayName: t.string('a', { minLength: 1, maxLength: 32 }),
});
// The values the schema validates against are readable back off it, so a form
// control can be driven by the same numbers instead of a second copy of them.
const age = t.at(SignupForm, 'age');
const min: 0 = age.constraints.min;
const max: 120 = age.constraints.max;
// A constraint that was not specified is `undefined` β not `number | undefined`
const step: undefined = age.constraints.step;
// The record's shape reaches the same member type directly
const maxLength: 32 = SignupForm.shape.displayName.constraints.maxLength;
What that gives you:
?. and at the literal type it was given (age.constraints.max is 120, not number | undefined). One that was not specified is typed undefined, so a typo or a missing constraint is a compile error rather than a silent undefined at runtime.t.refine and t.brand carry the base type's constraints over, and so do the branded constructors built on them β t.int(0, { min: 0, max: 100 }).constraints.max reads 100. Branding or refining a type that carries no constraints adds no constraints property at all.t.record and t.strictRecord expose the shape they were built from as .shape, and t.at(SignupForm, 'age') returns that very member type β constraints included. An optional member widens to T | undefined and still carries the member's constraints. Record types assembled at runtime β t.pick, t.omit, t.partial, t.mergeRecords, t.keyValueRecord β carry no shape, so t.at on one of those falls back to a plain Type as before.t.ConstraintsOf<typeof age> names the constraint record of a type, for passing it around or picking fields out of.Tip: If a default value violates its constraints,
ts-fortressthrows during construction. This guards against invalid schemas ever reaching production.
While ts-fortress, Zod, and io-ts are all excellent TypeScript validation libraries, ts-fortress offers more readable and informative error messages than both, a more type-safe way of building validators than Zod, and addresses some critical runtime consistency issues found in io-ts.
For more information, please see this documentation.
If you're coming from io-ts, here's how common patterns translate:
// io-ts style
import * as t from 'io-ts';
const User = t.type({
id: t.string,
name: t.string,
age: t.number,
});
type User = t.TypeOf<typeof User>;
// ts-fortress style
import * as t from 'ts-fortress';
const User = t.record({
id: t.string(),
name: t.string(),
age: t.number(20),
});
type User = t.TypeOf<typeof User>;
Key differences:
record instead of type, more explicit function namesResult type instead of EitherEvery validator in ts-fortress implements the Type<A> interface:
import type { StrictOmit } from 'ts-type-forge';
type Type<A> = Readonly<{
typeName: string; // Human-readable type name
defaultValue: A; // Default value for this type
is: (a: unknown) => a is A; // Type guard function
assertIs: (a: unknown) => asserts a is A; // Type assertion
cast: (a: unknown) => A; // Cast with fallback to default
fill: (a: unknown) => A; // Fill missing values with defaults
prune: <B extends A>(a: B) => A; // Recursively remove excess properties
validate: (a: unknown) => Result<A, readonly ValidationError[]>; // Detailed validation
}>;
validate - Detailed validation with error reportingThe validate method performs comprehensive validation and returns a Result type. When validation succeeds, it returns the original input object (same reference), preserving object identity:
import * as t from 'ts-fortress';
const User = t.record({
name: t.string(),
age: t.number(),
});
// Success case - validates correctly
const validData = { name: 'Alice', age: 30 } as const;
const result = User.validate(validData);
assert.isTrue(t.Result.isOk(result));
// A valid input is returned as is (the same reference)
assert.deepStrictEqual(result.value, { name: 'Alice', age: 30 });
assert.strictEqual(result.value, validData);
// Error case - provides detailed error information
const invalidData = { name: 'Bob', age: 'thirty' } as const;
const errorResult = User.validate(invalidData);
assert.isTrue(t.Result.isErr(errorResult));
assert.deepStrictEqual(errorResult.value, [
{
path: ['age'],
actualValue: 'thirty',
expectedType: 'number',
typeName: 'number',
details: undefined,
},
]);
assert.deepStrictEqual(t.validationErrorsToMessages(errorResult.value), [
'Error at age: expected <number> type but <string> type value "thirty" was passed.',
]);
assertIs - Type assertion with runtime checkingWhen using assertIs, you must assign it to a typed variable with an explicit type annotation due to TypeScript's limitations with assertion functions:
import * as t from 'ts-fortress';
const numberType = t.number();
// β
Correct usage - explicit type annotation required
const assertIsNumber: (a: unknown) => asserts a is number = numberType.assertIs;
const processValue = (value: unknown): void => {
assertIsNumber(value);
// After assertion, TypeScript knows value is a number
assertType<number>(value);
};
try {
processValue(42); // Works
processValue('not a number'); // Throws error
} catch (error) {
assert.deepStrictEqual(
error,
new Error(
'\nError: expected <number> type but <string> type value "not a number" was passed.',
),
);
}
// Example with complex types
const User = t.record({
id: t.string(),
name: t.string(),
});
type User = t.TypeOf<typeof User>;
// Explicit type annotation for the assertion function
const assertIsUser: (a: unknown) => asserts a is User = User.assertIs;
const processUser = (data: unknown): void => {
assertIsUser(data);
// TypeScript now knows data is User type
assertType<User>(data);
};
cast - Type casting with validationThe cast method validates the input and returns it if valid, otherwise throws an Error with validation details:
import * as t from 'ts-fortress';
const Port = t.number(8080);
assert.isTrue(Port.cast(3000) === 3000); // 3000 is a valid number
try {
Port.cast('invalid'); // Throws Error!
} catch (error) {
assert.deepStrictEqual(
error,
new Error(
'Error: expected <number> type but <string> type value "invalid" was passed.',
),
);
}
fill - Intelligent default value fillingThe fill method attempts to preserve valid parts of the input while filling in missing or invalid values with defaults.
See Default Values and Data Filling
defaultValue - Accessing the default valueEvery type has a defaultValue property that can be used for initialization:
import * as t from 'ts-fortress';
const User = t.record({
id: t.string(),
name: t.string('Guest'),
score: t.number(),
});
type User = t.TypeOf<typeof User>;
// Use defaultValue for initialization
const newUser: User = { ...User.defaultValue, id: 'user-123' } as const;
// This default value filling process can also be written as follows:
const newUser2: User = User.fill({ id: 'user-456' });
assert.deepStrictEqual(newUser, { id: 'user-123', name: 'Guest', score: 0 });
assert.deepStrictEqual(newUser2, { id: 'user-456', name: 'Guest', score: 0 });
// Useful for React state initialization
const UserForm = () => {
const [formData, setFormData] = useState<User>(User.defaultValue);
// ...
IGNORE_EMBEDDING(formData, setFormData);
};
import * as t from 'ts-fortress';
// Basic primitives
const stringType = t.string('default');
const numberType = t.number();
const booleanType = t.boolean(false);
const nullType = t.nullType;
const undefinedType = t.undefinedType;
// Literal types
const statusType = t.literal('active');
const versionType = t.literal(1);
// Arrays
const stringArrayType = t.array(t.string());
const nonEmptyArrayType = t.nonEmptyArray(t.number());
// Tuples
const coordinateType = t.tuple([t.number(), t.number()]);
import * as t from 'ts-fortress';
// Define object schemas
const Person = t.record({
firstName: t.string(),
lastName: t.string(),
age: t.number(),
address: t.record({
street: t.string(),
city: t.string(),
zipCode: t.string(),
}),
});
type Person = t.TypeOf<typeof Person>;
// Optional fields
const UserProfile = t.record({
username: t.string(),
bio: t.optional(t.string()), // Optional field
settings: t.partial(
t.record({
// Partial record (all fields optional)
theme: t.string('light'),
notifications: t.boolean(true),
}),
),
});
// Strict validation (disallow excess properties)
const StrictUserType = t.record(
{
id: t.string(),
name: t.string(),
},
{
excessProperty: 'reject', // Reject any properties not defined in schema
},
);
// Alternatively, use the strictRecord alias for cleaner syntax
const StrictUserTypeAlias = t.strictRecord({
id: t.string(),
name: t.string(),
});
// Permissive validation (allow excess properties)
const PermissiveUserType = t.record(
{
id: t.string(),
name: t.string(),
},
{
excessProperty: 'allow', // Allow additional properties and keep them in results
},
);
// Example usage - both StrictUserType and StrictUserTypeAlias behave identically
const strictData = { id: '123', name: 'John', extra: 'not allowed' } as const;
assert.isFalse(StrictUserType.is(strictData)); // 'extra' property causes rejection
assert.isFalse(StrictUserTypeAlias.is(strictData)); // same as above
const permissiveData = { id: '123', name: 'John', extra: 'allowed' } as const;
assert.isTrue(PermissiveUserType.is(permissiveData)); // 'extra' property is allowed
// strictRecord provides cleaner syntax for strict validation
const UserSchema = t.strictRecord({
name: t.string(),
email: t.string(),
age: t.number(),
});
// Validation examples
UserSchema.is({ name: 'John', email: 'john@example.com', age: 30 }); // β
true
UserSchema.is({
name: 'John',
email: 'john@example.com',
age: 30,
role: 'admin',
}); // β false - excess property
ts-fortress provides the refine function to create refined types with custom validation logic while leveraging existing base types:
import * as t from 'ts-fortress';
// Create refined types
const Uuid = t.refine({
baseType: t.string(),
// Define custom validation logic
is: (value: string): value is string =>
/^[\da-f]{8}-[\da-f]{4}-[0-5][\da-f]{3}-[089ab][\da-f]{3}-[\da-f]{12}$/iu.test(
value,
),
defaultValue: '00000000-1111-2222-3333-444444444444',
typeName: 'Uuid',
});
type Uuid = t.TypeOf<typeof Uuid>; // string (with runtime validation)
const PositiveNumber = t.refine({
baseType: t.number(1),
is: (value: number): value is number => value > 0,
defaultValue: 1,
typeName: 'PositiveNumber',
});
type PositiveNumber = t.TypeOf<typeof PositiveNumber>; // number (with runtime validation)
const EvenNumber = t.refine({
baseType: t.number(),
is: (value: number): value is number => value % 2 === 0,
defaultValue: 0,
typeName: 'EvenNumber',
});
// Usage in validation
const uuidResult = Uuid.validate('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b');
assert.isTrue(t.Result.isOk(uuidResult));
if (t.Result.isOk(uuidResult)) {
const validUuid = uuidResult.value; // string, guaranteed to be valid Uuid format
IGNORE_EMBEDDING(validUuid);
}
const positiveResult = PositiveNumber.validate(42);
assert.isTrue(t.Result.isOk(positiveResult));
if (t.Result.isOk(positiveResult)) {
const positiveNum = positiveResult.value; // number, guaranteed to be > 0
IGNORE_EMBEDDING(positiveNum);
}
// Invalid cases
assert.isFalse(Uuid.is('invalid-uuid'));
assert.isFalse(PositiveNumber.is(-5));
assert.isFalse(EvenNumber.is(7));
// Use in record schemas
const UserProfile = t.record({
id: Uuid, // refined uuid validation
score: PositiveNumber, // must be positive
level: EvenNumber, // must be even
});
type UserProfile = t.TypeOf<typeof UserProfile>;
// The refined types maintain their validation in composite types
const userData = {
id: '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b', // β
valid uuid format
score: 85, // β
positive number
level: 4, // β
even number
} as const satisfies UserProfile;
assert.isTrue(UserProfile.is(userData));
const invalidData = {
id: 'user123', // β invalid uuid format
score: -10, // β negative number
level: 3, // β odd number
} as const;
const result = UserProfile.validate(invalidData);
assert.isTrue(t.Result.isErr(result));
assert.deepStrictEqual(
t.validationErrorsToMessages(
result.value satisfies readonly t.ValidationError[],
),
[
'Error at id: expected <Uuid> type but <string> type value "user123" was passed.',
'Error at score: expected <PositiveNumber> type but <number> type value `-10` was passed.',
'Error at level: expected <EvenNumber> type but <number> type value `3` was passed.',
],
);
import * as t from 'ts-fortress';
// Domain-specific string types
const PhoneNumber = t.refine({
baseType: t.string(),
is: (s): s is string => /^\+?[\d\s()-]+$/u.test(s),
defaultValue: '+1234567890',
typeName: 'PhoneNumber',
});
const ZipCode = t.refine({
baseType: t.string(),
is: (s): s is string => /^\d{5}(-\d{4})?$/u.test(s),
defaultValue: '12345',
typeName: 'ZipCode',
});
// Constrained numeric types
const Percentage = t.refine({
baseType: t.number(),
is: (n): n is number => 0 <= n && n <= 100,
defaultValue: 0,
typeName: 'Percentage',
});
const Port = t.refine({
baseType: t.number(3000),
is: (n): n is number => Number.isInteger(n) && 1 <= n && n <= 65_535,
defaultValue: 3000,
typeName: 'Port',
});
ts-fortress provides extensive support for branded types to create domain-specific validation:
import * as t from 'ts-fortress';
// Simple branded types
const UserId = t.brandedString({ typeName: 'UserId', defaultValue: '' });
const Weight = t.brandedNumber({ typeName: 'Weight', defaultValue: 0 });
type UserId = t.TypeOf<typeof UserId>; // Brand<string, 'UserId'>
type Weight = t.TypeOf<typeof Weight>; // Brand<number, 'Weight'>
// Rich number validation types
const PositiveInt = t.positiveInt(1);
const SafeInt = t.safeInt(0);
const UInt16 = t.uint16(0);
// Usage
const userIdResult = UserId.validate('user_123');
assert.isTrue(t.Result.isOk(userIdResult));
if (t.Result.isOk(userIdResult)) {
const id: UserId = userIdResult.value;
IGNORE_EMBEDDING(id);
}
import * as t from 'ts-fortress';
// Union types
const IdType = t.union([t.string(), t.number()]);
// Intersection types
const TimestampedType = t.intersection(
[
t.record({ data: t.string() }),
t.record({
createdAt: t.number(Date.now()),
updatedAt: t.number(Date.now()),
}),
],
t.record({
data: t.string(),
createdAt: t.number(Date.now()),
updatedAt: t.number(Date.now()),
}),
);
// Merge records (similar to intersection but more specific)
const ExtendedUserType = t.mergeRecords([
PersonType,
t.record({
id: t.string(),
email: t.string(),
}),
]);
import * as t from 'ts-fortress';
// String enums
const ColorEnum = t.enumType(['red', 'green', 'blue']);
type Color = t.TypeOf<typeof ColorEnum>; // 'red' | 'green' | 'blue'
// Numeric ranges (end is exclusive)
const DiceRoll = t.uintRange(1, 7, { defaultValue: 1 }); // integers from 1 to 6
type DiceRoll = t.TypeOf<typeof DiceRoll>; // 1 | 2 | 3 | 4 | 5 | 6
// Numeric ranges (end is inclusive)
const DiceRoll2 = t.uintRangeInclusive(1, 6, { defaultValue: 1 }); // integers from 1 to 6
type DiceRoll2 = t.TypeOf<typeof DiceRoll2>; // 1 | 2 | 3 | 4 | 5 | 6
Tips: It is often better to use uintRange instead of enumType when possible, because enumType stores a Set of its members as data, while uintRange only stores the range, resulting in smaller memory usage.
ts-fortress uses Result<T, readonly ValidationError[]> for structured error handling with detailed error information:
import * as t from 'ts-fortress';
const User = t.record({
name: t.string(),
age: t.number(),
});
type User = t.TypeOf<typeof User>;
const invalidData = { name: 123, age: 'not a number' } as const;
const result = User.validate(invalidData);
assert.isTrue(t.Result.isErr(result));
// result.value is an array of ValidationError objects
assert.deepStrictEqual(result.value, [
{
actualValue: 123,
expectedType: 'string',
path: ['name'],
typeName: 'string',
details: undefined,
},
{
actualValue: 'not a number',
expectedType: 'number',
path: ['age'],
typeName: 'number',
details: undefined,
},
] satisfies readonly t.ValidationError[]);
// Convert to string messages
const messages = t.validationErrorsToMessages(result.value);
assert.deepStrictEqual(messages, [
'Error at name: expected <string> type but <number> type value `123` was passed.',
'Error at age: expected <number> type but <string> type value "not a number" was passed.',
]);
const assertIsUser: (a: unknown) => asserts a is User = User.assertIs;
// Using assertions (throws on invalid data)
try {
assertIsUser(invalidData);
} catch (error) {
assert.deepStrictEqual(
error,
new Error(
'\nError at name: expected <string> type but <number> type value `123` was passed.,\nError at age: expected <number> type but <string> type value "not a number" was passed.',
),
);
}
// Excess property validation example
const StrictType = t.record(
{
name: t.string(),
age: t.number(),
},
{
excessProperty: 'reject',
},
);
const dataWithExcess = { name: 'John', age: 30, extra: 'not allowed' } as const;
const strictResult = StrictType.validate(dataWithExcess);
assert.isTrue(t.Result.isErr(strictResult));
assert.deepStrictEqual(strictResult.value, [
{
path: ['extra'],
actualValue: 'not allowed',
expectedType: '{ name: string, age: number }',
typeName: '{ name: string, age: number }',
details: {
kind: 'excess-key',
key: 'extra',
},
},
]);
Each validation error provides detailed information:
type ValidationError = Readonly<{
path: readonly string[];
actualValue: unknown; // The actual value that failed validation
expectedType: string; // The expected type or constraint
typeName: string; // Name of the type being validated
details?: ValidationErrorDetails | undefined; // Structured information used to build a descriptive message
}>;
t.string(defaultValue) - String validationt.number(defaultValue) - Number validationt.boolean(defaultValue) - Boolean validationt.nullType / t.undefinedType - Null/undefined validationt.literal(value) - Literal types (string, number, or boolean)t.array(elementType) - Array validationt.nonEmptyArray(elementType) - Non-empty array validationt.tuple([t1, t2, ..., tN]) - Fixed-length tuple validationt.fixedLengthTuple(size, elementType) - Fixed-length array validationt.minLengthTuple(size, elementType) - Array validation with a minimum lengtht.maxLengthTuple(size, elementType) - Array validation with a maximum lengtht.boundedLengthTuple(min, max, elementType) - Array validation with a length between min and max (inclusive)t.record(schema, options?) - Object validation
options.excessProperty?: 'allow' | 'reject' - How properties not defined in the schema are handled (default: 'allow')t.strictRecord(schema, options?) - Object validation with strict mode (alias for record with excessProperty: 'reject')t.keyValueRecord(keyType, valueType) - Corresponding to the Record<K, V> typet.partial(recordType) - Make all fields optionalt.optional(type) - Optional field wrappert.pick(recordType, keys) - Pick specific fieldst.omit(recordType, keys) - Omit specific fieldst.keyof(recordType) - Key of the record type.t.at(recordType, key) - The member type stored at key. On a record built by t.record / t.strictRecord it returns the shape's own member type, constraints and all; those record types also expose .shape directly.t.union(types) - Union type validationt.intersection(types, defaultType) - Intersection type validationt.mergeRecords(recordTypes) - Merge multiple record typest.refine({ baseType, is, defaultValue }) - Refine baseType by is functiont.brand({ baseType, is, defaultValue, brandKeys, brandFalseKeys?, typeName? }) - Refine baseType by is function with brand typingt.brandedString({ typeName, defaultValue, is? }) - String brandingt.brandedNumber({ typeName, defaultValue, is? }) - Number brandingt.int(), t.safeInt(), t.positiveInt(), t.uint16(), etc.t.TypeOf<T> - Extract TypeScript type from validatortype.constraints - The constraints a t.string / t.number / t.bigint type (or a t.refine / t.brand wrapper of one) was created with. Every constraint key is present; the unspecified ones are typed undefined. See Reading constraints backt.ConstraintsOf<T> - The constraint record carried by a typet.attachConstraints(type, constraints) - Attach constraint values to a type of your ownt.enumType(values) - Enum validationt.uintRange(start, end, { defaultValue? }) - Non-negative integer range validation (end is exclusive)t.intRange(start, end, { defaultValue? }) - Integer range validation (end is exclusive)t.uintRangeInclusive(start, end, { defaultValue? }) - Non-negative integer range validation (end is inclusive)t.intRangeInclusive(start, end, { defaultValue? }) - Integer range validation (end is inclusive)t.unknown() - Unknown Typet.recursion(typeName, definition) - Define recursive typet.int8() / t.uint8() - Int8 / Uint8t.JsonValue / t.JsonPrimitive / t.JsonObjectt.nullable(T) - An alias of t.union([T, t.undefinedType])We welcome contributions! Please open an issue or a pull request in the noshiro-pf/mono repository.
This project is licensed under the Apache-2.0 License - see the LICENSE file for details.