safe-validate
v1.0.4
Published
Runtime validation types for TypeScript.
Maintainers
Readme
safe-validate
Runtime validation and type inference for TypeScript.
safe-validate is a strongly typed validation library that models data shapes with function-based validators. Schemas validate at runtime and infer accurate TypeScript types at compile time, so you define your data contract once and use it in both places.
Previously published as Funval.
Why safe-validate?
| safe-validate | Joi (example) |
| --- | --- |
| Types inferred from schemas | Types defined separately |
| Native TypeScript-style validators (string, number, array) | Joi-specific API |
| Composable validator chains | Chainable, but separate from TS types |
| Zero runtime dependencies | External dependency tree |
// safe-validate
const UserSchema = Schema({
name: string,
amount: number,
flags: array.of(string).optional(),
});
type User = Type<typeof UserSchema>;// Joi equivalent
const UserSchema = Joi.object({
name: Joi.string().required(),
amount: Joi.number().required(),
flags: Joi.array().items(Joi.string()),
});
type User = {
name: string;
amount: number;
flags?: string[];
};Features
- Readable schemas — Validators mirror TypeScript primitives (
string,number,boolean,array,unknown, and more). - Less duplication — Reuse and compose validators to build new types quickly.
- Compile-time safety — TypeScript catches invalid schema usage before runtime.
- Composable chains — Combine validators to transform and validate data in one pipeline.
- Sync and async — Promise-returning validators are detected automatically.
- Zero dependencies — Small runtime footprint.
- Plain JavaScript — Works in projects with or without TypeScript.
Table of Contents
Installation
npm install safe-validateRequires Node.js 12 or later.
Quick Start
import Schema, { Type, string, number, array } from 'safe-validate';
const UserSchema = Schema({
name: string.trim().normalize().between(3, 40).optional(),
username: /^[a-z0-9]{3,10}$/,
status: Schema.either('active' as const, 'suspended' as const),
items: array
.of({
id: string,
amount: number.gte(1).integer(),
})
.min(1),
});
type User = Type<typeof UserSchema>;
const validator = UserSchema.destruct();
const [err, user] = validator({
username: 'john1',
// TypeScript error: '"unregistered"' is not assignable to '"active" | "suspended"'
status: 'unregistered',
items: [{ id: 'item-1', amount: 20 }],
});
console.log(err);
// ValidationError: status: Expect value to equal "suspended"Creating Custom Validators
Define a function that accepts unknown, validates the input, and returns the typed value:
import * as EmailValidator from 'email-validator';
function Email(input: unknown): string {
if (!EmailValidator.validate(String(input))) {
throw new TypeError(`Invalid email address: "${input}"`);
}
return input as string;
}Use it in a schema:
const UserSchema = Schema({
email: Email,
});For optional fields, widen the parameter type:
function OptionalEmail(input?: unknown): string | undefined {
return input == null ? undefined : Email(input);
}Using .transform()
Wrap a custom validator with .transform() to enable chaining:
const EmailWithValidatorChain = unknown.string.transform(Email);
const UserSchema = Schema({
email: EmailWithValidatorChain.optional().max(100),
});Asynchronous Validators
Return a Promise (or PromiseLike) from a validator to enable async validation:
async function AvailableUsername(input: string): Promise<string> {
const res = await fetch(
`/check-username?username=${encodeURIComponent(input)}`,
);
if (!res.ok) {
throw new TypeError(`Username "${input}" is already taken`);
}
return input;
}
const UserSchema = Schema({
username: AvailableUsername,
});
const user = await UserSchema({ username: 'test' });safe-validate propagates async return types through the schema. Accessing a promise result without await is flagged by TypeScript.
Validator Chains
Every built-in validator is a callable function with chainable helpers. Chaining runs validators in order and updates the inferred type as transforms are applied.
import { unknown } from 'safe-validate';
const validator = unknown.number().gt(0).toFixed(2);
console.log(validator('123.4567')); // '123.46'After .toFixed(), the validator returns a string, so subsequent chain methods are string helpers.
Common chain methods
| Method | Description |
| --- | --- |
| .equals() | Assert the value equals a given value |
| .test() | Run a custom predicate |
| .transform() | Map the validated value to a new value |
| .construct() | Reshape arguments before validation |
| .optional() | Allow null or undefined |
| .strictOptional() | Allow only undefined |
| .destruct() | Return [error, value] instead of throwing |
| .error() | Replace thrown errors with a custom message |
.equals()
const validator = boolean.equals(true);.test()
import * as EmailValidator from 'email-validator';
const validator = string.test(EmailValidator.validate, 'Invalid email address');.transform()
const validator = number.transform((x): number => {
if (x <= 0) {
throw new RangeError('Expected number to be positive');
}
return Math.sqrt(x);
});.construct()
Reshape validator arguments before the underlying validator runs. The construct function must return an array of arguments.
const validator = number.gt(1).construct((x: number, y: number) => [x + y]);
validator(1, 2); // validates 3.optional()
const validator = Schema({
name: string.trim().min(1),
address: string.trim().optional(),
});.strictOptional()
Same as .optional(), but only undefined is accepted (not null).
const validator = Schema({
name: string.trim().min(1),
address: string.trim().strictOptional(),
});.destruct()
Return a tuple [error, value] instead of throwing on validation failure.
const validator = Schema({
name: string.trim().min(1),
}).destruct();
const [err, user] = validator(req.body);.error()
Replace validation errors with a custom message, ValidationError, or error factory.
const validator = Schema({
name: string.error('expect input to be string'),
amount: number.gt(0, (val) => `${val} is not a positive amount`),
});API Reference
Import the primitives you need:
import Schema, {
unknown,
string,
number,
boolean,
array,
DateType,
} from 'safe-validate';Schema
Create a validator from a schema object, literal values, or function validators.
const validator = Schema(
{
name: string,
amount: number,
},
'Missing name or amount',
);Strict mode — Reject properties not defined on the schema:
const validator = Schema(
{
name: string,
amount: number,
},
{ strict: true },
);Schema.either — Validate one of several shapes (OR):
const validator = Schema.either({ foo: string }, { bar: number });
// { foo: string } | { bar: number }Schema.merge — Merge multiple schemas (AND):
const validator = Schema.merge({ foo: string }, { bar: number });
// { foo: string; bar: number }Schema.enum — Validate against a TypeScript enum:
enum Status {
OK,
Invalid,
}
const validator = Schema.enum(Status, 'Invalid status');Schema.record — Validate a Record<key, value>:
const validator = Schema.record(string.regexp(/^[a-z]+$/), number);unknown
Accept any value and coerce or validate it:
const validator = Schema({ data: unknown });| Method | Description |
| --- | --- |
| unknown.schema() | Coerce to a nested schema |
| unknown.object() | Coerce to an object |
| unknown.array() | Coerce to an array |
| unknown.string() | Coerce to a string |
| unknown.number() | Coerce to a number |
| unknown.boolean() | Coerce to a boolean |
| unknown.date() | Coerce to a Date |
| unknown.enum() | Coerce to an enum value |
| unknown.record() | Coerce to a record |
const validator = unknown.string('Expect data to be string').toUpperCase();
// accepts { data: 1 } and converts to { data: '1' }string
Accept string values (including empty strings).
| Method | Description |
| --- | --- |
| toLowerCase() / toUpperCase() | Change case |
| toLocaleLowerCase() / toLocaleUpperCase() | Locale-aware case change |
| trim() | Remove leading and trailing whitespace |
| truncate(n) | Truncate with ellipsis |
| normalize() | Unicode normalization |
| min(n) / max(n) / between(min, max) | Length constraints |
| regexp(pattern) | Match a regular expression |
number
Accept numeric values.
| Method | Description |
| --- | --- |
| float() | Accept floats (reject NaN and non-finite values) |
| integer() | Accept integers only |
| toExponential() / toFixed() / toPrecision() | Format as string |
| toLocaleString() | Locale-formatted string |
| toString(radix?) | Convert to string |
| gte() / lte() / gt() / lt() / between() | Range constraints |
boolean
Accept boolean values.
const validator = Schema({ agree: boolean });array
Accept array values.
| Method | Description |
| --- | --- |
| of(schema) | Validate each element |
| min(n) / max(n) / between(min, max) | Length constraints |
const numbers = array.of(number);
const tuple = array.of(number).between(1, 2);
const objects = array.of({ foo: number });
const enums = array.of(Schema.enum(Status));DateType
Accept Date instances.
| Method | Description |
| --- | --- |
| toISOString() | Convert to ISO string |
| getTime() | Convert to timestamp |
| gte() / lte() / gt() / lt() / between() | Date range constraints |
const validator = Schema({ eventTime: DateType });Development
npm install # install dependencies and build
npm test # type-check, lint, and run tests
npm run build # compile to lib/