New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

reviewed

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

reviewed

Ergonomic, extensible and lightweight validators.

latest
Source
npmnpm
Version
1.2.1
Version published
Weekly downloads
4K
-66.55%
Maintainers
1
Weekly downloads
 
Created
Source

Reviewed

Ergonomic, extensible and lightweight validators.

Review Version Downloads Size Quality Coverage

Motivation

  • I want to validate unknowns into plain objects I can consume:
import { isRecordOf, isString } from "reviewed";

const isPerson = isRecordOf({
  name: isString,
});

isPerson({ name: "Joel" });
{
  "valid": true,
  "input": { "name": "Joel" },
  "parsed": { "name": "Joel" },
  "error": null
}

If I pass something invalid it should give me consumable errors:

isPerson({ name: null });
{
  "valid": false,
  "input": { "name": null },
  "parsed": null,
  "error": {
    "name": "Not a string: null"
  }
}
  • I want the compiler be able to infer all the types:
const { valid, parsed } = isPerson({ name: "Joel" });

if (valid) {
  // Type: { page: number, size: number }
  console.log(parsed);

  // Type: number
  console.log(parsed.page);
}
  • During validation I want to parse the data:

Parsing is already part of validation. isNaturalNumberString checks a value is a whole number saved as a string for example as a URL parameter.

Since the validator needs to assess the value contains a number already, it should be responsible for parsing it to a number type.

import { isRecordOf, isNaturalNumberString } from "reviewed";

const paginate = (url: URL): void => {
  const isPagination = isRecordOf({
    page: isNaturalNumberString,
    size: isNaturalNumberString,
  });

  const { valid, parsed, error } = isPagination({
    page: url.searchParams.get("page"),
    size: url.searchParams.get("size"),
  });

  if (valid) {
    // Parsed type: { page: number, size: number }
    console.log(parsed);
  } else {
    console.error(error);
  }
};

Now I lift numbers directly from the url:

paginate(new URL("https://example.com?page=1&size=10"));
{
  "page": 1,
  "size": 10
}

And I can get useful error messages:

paginate(new URL("https://example.com?page=-1"));
{
  "page": "Not a natural number string: '-1'",
  "size": "Not a string: null"
}

Installing

npm install reviewed

Documentation

Documentation and more detailed examples are hosted on Github Pages.

Design

A validation library for TypeScript needs to be:

  • Ergonomic
    • Validated types can be inferred by the compiler
    • Validators can parse the inputs
    • Errors are easy to collect
  • Extensible
    • It is quick to write validators
    • It is simple to test validators
    • Common validators are available
  • Lightweight
    • Tiny bundle size
    • Fully tree shakeable

This package exposes a flexible interface that achieves these goals.

Alternatives

Webpack warns when a bundle exceeds 250kb, validation is not an optional feature of an application. If the minified size of a package compromises this budget it simply won't be used.

PackageVersionMinified (kB)
joi17.12.0145.5
ajv8.12.0119.6
validator13.11.0114.2
zod3.22.457.0
yup1.3.340.8
superstruct1.0.311.5

Superstruct has good TypeScript support and serves as an inspiration for this package. However, the validation style for this package is designed to be much simpler and more flexible than superstruct with less need for factory functions and simpler failure message customisation.

Usage

Writing validators

numbers.ts

import { Validator, isInteger, validateIf } from "reviewed";

export const isNaturalNumber: Validator<number> = (input: unknown) => {
  const integer = isInteger(input);

  if (!integer.valid) {
    return integer;
  }

  return validateIf(integer.parsed > 0, input, input, "Not a natural number");
};

Combining validators

Validators can be chained to validate a payload:

[{ "name": "a" }, { "name": "b" }]
export const isArrayOfNames = isArrayOf(
  isRecordOf({
    name: isString,
  }),
);

Assertions

Validators can be told to throw an error if validation fails:

import { assert, isNumber } from "reviewed";

const parsed = assert(isNumber, x);

// Parsed type: number
console.log(parsed + 1);

Guards

Guards can inform the compiler that the input satisfies a type predicate. This is thanks to TypeScript's is operator:

(input: unknown): input is string => {};

Validators already let the compiler know the output matches the validated type:

import { isNumber } from "reviewed";

const { valid, parsed } = isNumber(x);

if (valid) {
  // Parsed type: number
  console.log(parsed + 1);
}

If we convert this to a guard, the compiler will also confirm the input has the matching type:

import { guard, isNumber } from "reviewed";

if (guard(isNumber, input)) {
  // Parsed type: number
  console.log(input + 1);
}

Optional fields

Validators can be made optional:

interface Person {
  name?: string;
}

const isPerson = isRecordOf<Person>({ name: optional(isString) });
isPerson({});
{
  "valid": true,
  "parsed": { "name": "Joel" }
}
isPerson({ name: "Joel" });
{
  "valid": true,
  "parsed": { "name": "Joel" }
}

Strictly speaking, { name: optional(isString) } implies that the interface is { name: string | undefined } which allows name to be explicitly undefined. Since this is never useful the interface is interpreted as being { name?: string } which is simpler than having a separate function for strictly optional values.

Note that there has been lots of discussion around changing the way TypeScript handles undefined vs optional parameters: https://github.com/Microsoft/TypeScript/issues/12400. Rather than implementing some casts under the hood to fight the type checker this library leaves it up to the developer to be explicit:

const isPerson = isRecordOf<Person>({ name: optional(isString) });

Whereas this will throw an error when strict optional checking is enabled:

const isPerson: Validator<Person> = isRecordOf({ name: optional(isString) });

Using literals

Array literals can be converted directly to validators:

const builds = ["dev", "prod"] as const;
const isBuild = isOneOf(builds);
isBuild("dev");
{
  "valid": true,
  "parsed": [3, 1]
}
isBuild("local");
{
  "valid": false,
  "error": "Not one of ['dev', 'prod']: 'local'"
}

Testing validators

Custom Jest matchers are exposed for testing validators:

jest.config.json

{
  "setupFilesAfterEnv": ["reviewed/dist/testing/globals.js"]
}

tsconfig.json

{
  "files": ["node_modules/reviewed/dist/testing/globals.d.ts"]
}
import { isNaturalNumberString } from "./strings";

describe("isNaturalNumberString", () => {
  it("parses natural number strings", () => {
    expect(isNaturalNumberString).toValidate("1");
    expect(isNaturalNumberString).toInvalidate({});
    expect(isNaturalNumberString).toValidateAs("1", 1);
    expect(isNaturalNumberString).toInvalidateWith({}, "Not a string");
  });
});

Overview

Validators take an unknown input and return a record that implements the Valid or Invalid interfaces:

interface Valid<T> {
  valid: true;
  input: unknown;
  parsed: T;
  error: null;
}

interface Invalid<T> {
  valid: false;
  input: unknown;
  parsed: null;
  error: ValidationErrors<T>;
}

type Validated<T> = Valid<T> | Invalid<T>;

type Validator<T> = (input: unknown) => Validated<T>;

Inputs can be validated directly:

const validate: <T>(input: unknown, parsed: unknown = input) => Validated<T>;
const invalidate: <T>(input: unknown, error: ValidationErrors<T>) => Validated<T>;

const validateWith: <T>(validators: ValidatorFields<T>) => Validator<T>;
const invalidateWith: <T>(reason: string) => Validator<T>;

Helper factories are provided:

const validateIf: <T>(condition: boolean, input: unknown, parsed: unknown, reason: string) => Validated<T[]>;
const validateRegex: <T extends string>(regex: RegExp, reason: string) => RegexValidator<T>;

const validateAll: <T>(validator: Validator<T>) => Validator<T[]>;
const validateEach: <T>(validator: Validator<T>) => Validator<T>[];

const validateOr: <T>(validator: Validator<T>, fallback: T) => (input: unknown) => T;
const validateEachOr: <T>(validator: Validator<T>, fallback: T) => (input: unknown) => T[];

Validators can be used to filter inputs:

const filterValid: <T>(validator: Validator<T>) => (input: unknown) => T[];

Validators can be inverted or joined:

const not: <T>(validator: Validator<T>, reason?: string) => Validator<T>;

const both: <T, U>(first: Validator<T>, second: Validator<U>) => Validator<T & U>;
const either: <T, U>(first: Validator<T>, second: Validator<U>) => Validator<T | U>;

const optional = <T>(validator: Validator<T>): Validator<T | undefined>;

const isArrayOf: <T>(validator: Validator<T>) => Validator<T[]>;
const isRecordOf: <T>(validators: ValidatorFields<T>) => Validator<T>;

Results can be merged:

const all: <T>(results: Validated<T>[]) => Validated<T[]>;
const any: <T>(results: Validated<T>[]) => Validated<T[]>;

const merge: <T>(results: ValidatedFields<T>) => Validated<T>;
const sieve: <T>(results: ValidatedFields<T>) => Partial<T>;

Validation errors can be converted to native errors:

const fail: <T>(errors: ValidationErrors<T>) => Error;

Common validators are provided out of the box:

const isUndefined: Validator<undefined>;
const isNull: Validator<null>;
const isBoolean: Validator<boolean>;
const isNumber: Validator<number>;
const isString: Validator<string>;
const isObject: Validator<object>;

const isInteger: Validator<number>;
const isNaturalNumber: Validator<number>;

const isBooleanString: Validator<boolean>;
const isNumberString: Validator<number>;
const isIntegerString: Validator<number>;
const isNaturalNumberString: Validator<number>;
const isJSONString: Validator<IJSON>;

const isArray: Validator<unknown[]>;
const isNonEmptyArray: Validator<unknown[]>;
const isNumberArray: Validator<number[]>;
const isStringArray: Validator<string[]>;

const isRecord: Validator<Record<string, unknown>>;
const isNonEmptyRecord: Validator<Record<string, unknown>>;

const isEmail: RegexValidator<"user" | "domain">;

const isOneOf: <T>(options: T[]) => Validator<T>;
const isManyOf: <T>(options: T[]) => Validator<T[]>;

Discussion

Reasonable types

JavaScript has some famously confusing types:

typeof NaN;
"number"

Care is taken to make primitive types easier to work with.

Numbers

const isNumber: Validator<number> = (input: unknown) =>
  validateIf(typeof input === "number" && isFinite(input), input, input, "Not a number");
InputParsedError
11null
NaNnullNot an number
InfinitynullNot an number
""nullNot an number

Objects

const isObject: Validator<object> = (input: unknown) =>
  validateIf(typeof input === "object" && input !== null, input, input, "Not an object");
InputParsedError
[][]null
{}{}null
""nullNot an object

Records

const isRecord: Validator<Record<string, unknown>> = (input: unknown) =>
  validateIf(isObject(input).valid && !isArray(input).valid, input, input, "Not a record");
InputParsedError
[]nullNot a record
{}{}null
""nullNot an object

Secret magic

Hey let's write an isArrayOf and isRecordOf function:

const isArrayOf: <T>(validator: Validator<T>) => Validator<T[]>;
const isRecordOf: <T>(validators: ValidatorFields<T>) => Validator<T>;
const isRecordOfAtLeast: <T>(validators: ValidatorFields<T>) => Validator<T>;

But wait we already have:

const validateAll: <T>(validator: Validator<T>) => Validator<T[]>;
const validateWith: <T>(validators: ValidatorFields<T>) => Validator<T>;
const validateWithAtLeast: <T>(validators: ValidatorFields<T>) => Validator<T>;

That's because they're the same thing woah...

Celebration

So we can just alias them:

export const isArrayOf = validateAll;
export const isRecordOf = validateWith;
export const isRecordOfAtLeast = validateWithAtLeast;

Tooling

Dependencies

To install dependencies:

yarn install

Tests

To run tests:

yarn test

Documentation

To generate the documentation locally:

yarn docs

Linters

To run linters:

yarn lint

Formatters

To run formatters:

yarn format

Contributing

Please read this repository's Code of Conduct which outlines our collaboration standards and the Changelog for details on breaking changes that have been made.

This repository adheres to semantic versioning standards. For more information on semantic versioning visit SemVer.

Bump2version is used to version and tag changes. For example:

bump2version patch

Contributors

Remarks

Lots of love to the open source community!

Be kind to your mind Love each other It's ok to have a bad day

Keywords

validators

FAQs

Package last updated on 20 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts