npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

safe-validate

v1.0.4

Published

Runtime validation types for TypeScript.

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

Requires 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/