Skip to main content

A simple Deno 🦕 TypeScript runtime validator with good performance, zero dependencies and developer friendly UX.

Works with
It is unknown whether this package works with Bun
It is unknown whether this package works with Cloudflare Workers
It is unknown whether this package works with Node.js
This package works with Deno
It is unknown whether this package works with Browsers
JSR Score
64%
Published
3 months ago (0.8.2)

Test

Rules

A simple Deno 🦕 TypeScript runtime validator with good performance, zero dependencies and developer friendly UX.

Build a schema using predefined rules, or create your own. Valid data will be returned typed.

Getting started

import { parse, str } from 'https://deno.land/x/rules/mod.ts'

Using the rule str() we can validate that any given data is of type string. The parse method will always return an array containing any errors (or undefined if there are none) followed by the validated data itself (or undefined if invalid).

parse(str(), "Homer");

We can also validate more complex structures such as objects and arrays.

const User = obj({
  name: str(),
  knownAs: array(str()),
  age: num(),
});

const data = {
  name: "Homer",
  knownAs: ["Max Power", "Pie Man", "Mr. Plow"],
  age: 39,
};

parse(User, data);

At some point you'll likely want your own custom validation logic. Using refine will allow you to do just that, by building on top of existing rules.

// A custom, reusable validation rule
const email = refine("email", str(), (ctx) => {
  if (!/\S+@\S+\.\S+/.test(ctx.value)) {
    return ctx.error(
      "invalid_email",
      "Must be a valid email"
    )
  }
  return ctx.success();
});

const User = obj({
  name: str(),
  email: email,
});

const data = {
  name: "Homer",
  email: "chunkylover53@aol.com",
};

parse(User, data);

Documentation

Validation

parse

Provide a schema and value to parse(schema, value) to validate your data. A tuple of [errors, validated_data] will be returned. If errors are present, validated_data will be undefined. If your data is valid, errors will be undefined.

const schema = obj({name: str()});
const [errors, user] = parse(schema, { name: "Homer" });

// errors
undefined

// user
{
  name: "Homer"
}
const schema = obj({name: str()});
const [errors, user] = parse(schema, {});

// errors
Map {
  "name" => [
    {
      name: "str",
      value: undefined,
      path: ["name"],
      code: "required",
      message: "is required"
    }
  ]
}

// user
undefined

Errors

Errors are returned as a Map of keys. Each key holds an array of error object(s).

Map {
  "name" => [
    {
      name: "str",
      value: undefined,
      path: ["name"],
      code: "required",
      message: "is required"
    }
  ]
}

error.name

error.name contains the name of the rule.

error.value

error.value contains the provided value.

error.path

error.path contains the path taken. This will be an array of property names and / or an index indicating the location of the value that caused the error.

error.code

error.code provides a code that can be used to reference the type of error. See the predefined error codes list for a list of valid codes.

error.message

error.message contains the error message.

There are default error messages for each error code type. They can be easily overridden by passing the error code with "_error" appended as a property to each rule. This could also be used for I18n.

str({required_error: "must be provided"})

There is also a helper available to format error messages in a more user friendly way. See the format helper for more information.

Context

Context is available within all rules (and utils).

ctx.value

ctx.value contains the value.

ctx.path

ctx.path contains the path taken. This will be an array of property names and / or an index indicating the location of the value that caused the error.

ctx.error

ctx.error(code, message, meta) takes a code (an error code string), an error message and a meta object to hold additional details.

ctx.success

ctx.success returns a success object.

Rules

any

Allow any value as valid.

any()

array

Allow an array of values. A min and / or max array length can also be specified.

array(str(), { min: 1, max: 3 })

bigInt

Must be a bigint value e.g. 100n.

bigInt()

boolean

Value must be either true or false.

bool()

enum

Value must be one of the given literal values.

enums(["Homer", "Max Power", "Mr. Plow"] as const

literal

Value must be an exact match (using ===).

literal(742)

never

Validation will always fail.

never()

num

Value must be a number. A min and / or max value can also be specified. If the number must be an integer, set integer to true.

num({min: 5, max: 10, integer: true})

obj

Validate that the provided value is an object, and that each property is also valid. Can be nested. Any unknown or undefined properties will be excluded from the returned object.

obj({
  name: str(),
  knownAs: array(str()),
  age: num(),
});

regex

Value must be a string that passes the provided regular expression.

regex(/Bart/i)

str

Value must be a string. A min and / or max length can also be specified. The string can also be trimmed of whitespace via trim: true .

str({min: 5, max: 10, trim: true})

tuple

Validates that a value is an array of the same length, with each value being the given type at that position.

tuple([str(), num()])

Utils

coerce

Coerce a value into another allowing you to transform input data (before validation).

The example below attempts to coerce the input to number, before passing the value on to be validated. If the value cannot be coerced, it is returned as is.

coerce(num(), (value) => {
  if (typeof value === "string") {
    return parseInt(value, 10);
  } else {
    return value;
  }
})

The coercion function can also be provided separately.

const coerceFn: coerceFn<number> = (value) => {
  if (typeof value === "string") {
    return parseInt(value, 10);
  } else {
    return value;
  }
};

coerce(str(), coerceFn);

defaulted

Provide a default if it's undefined (before validation).

defaulted(str(), (ctx) => "30")

The defaulted function can also be provided separately.

const defaultedFn: defaultedFn<string> = (ctx) => "30";

defaulted(str(), defaultedFn);

dynamic

Decide what validation to run at runtime.

const Homer = obj({
  name: literal("Homer"),
  catchphrase: literal("D'oh")
});

const Character = obj({
  name: str(),
  catchphrase: str()
});

dynamic((ctx) => {
  if (ctx.value?.name === "Homer") {
    return Homer;
  } else {
    return Character;
  }
})

The dynamic function can also be provided separately.

const dynamicCb: dynamicFn<typeof Homer | typeof Character> = (ctx) => {
  if (ctx.value?.name === "Homer") {
    return Homer;
  } else {
    return Character;
  }
};

intersection

Used to validate that a group of rules pass.

intersection([
  str({ min: 4 }),
  regex(/Bart/i)
])

nullable

Allow a value to be null.

nullable(str())

optional

Allow a value to be undefined.

optional(str())

refine

Allow an existing rule to be refined. Useful for defining your own rules.

refine("email", str(), (ctx) => {
  if (!/\S+@\S+\.\S+/.test(ctx.value)) {
    return ctx.error(
      "invalid_email",
      "Must be a valid email"
    )
  }
  return ctx.success();
});

Your refinement can also be provided separately.

const refineCb: refineCb<string> = (ctx) => {
  // Is it a valid email?
  if (!/\S+@\S+\.\S+/.test(ctx.value)) {
    return ctx.error(
      "invalid_email",
      "Must be a valid email"
    )
  }
  return ctx.success();
};

refine("email", str(), refineCb);

union

Validate that a value matches at least one rule.

union([
  literal("Homer"),
  literal("Marge")
])

Helpers

format

format can be used to generate a nice Map of error codes and messages. Path name will be appended to the start of the error message and union errors are combined into a single message.

By default error messages will be sentance cased and any . or _ replaced with a whitespace. You can pass { humanise: false } as a second argument to prevent this.

const schema = obj({
  account: obj({
    first_name: str()
  })
});
const [errors, user] = parse(schema, { account: {} });

format(errors);

// Output
Map { "account.first_name" => [ { code: "required", message: "Account first name is required" } ] }

isValid

isValid can be used as a helpful type guard for narrowing the result.

const result = parse(schema, { name: "Homer" })

if(isValid(result)) {
  const name = result[1].name
}

Infer

Infer can be used as a TypeScript util to infer the return type of a Rule.

const homer = obj({
  name: literal("Homer"),
  catchphrase: literal("D'oh")
});

type Homer = Infer<typeof homer>;

// Type is now:
{
  name: "Homer";
  catchphrase: "D'oh";
}

Codes

Predefined error codes.

required invalid invalid_type invalid_min_length invalid_max_length invalid_enum invalid_union invalid_integer invalid_literal invalid_length regex_no_match

Contributing

Tests

Run tests manually with deno task test

Add Package

deno add @rossholdway/rules

Import symbol

import * as mod from "@rossholdway/rules";

Add Package

npx jsr add @rossholdway/rules

Import symbol

import * as mod from "@rossholdway/rules";

Add Package

yarn dlx jsr add @rossholdway/rules

Import symbol

import * as mod from "@rossholdway/rules";

Add Package

pnpm dlx jsr add @rossholdway/rules

Import symbol

import * as mod from "@rossholdway/rules";

Add Package

bunx jsr add @rossholdway/rules

Import symbol

import * as mod from "@rossholdway/rules";