Skip to content

The 0 dependency recipe for type strong validations.

Notifications You must be signed in to change notification settings

hongshancapital/power-guard

Repository files navigation

Power Guard

The 0 dependency recipe for type strong validations: from unknown to anything you want. 中文

CI version size

🤔 A Cozy Way to Validate Unknown Variables

Instead of having:

const validate = (x: unknown): SomeType => {
  if (!(typeof x === 'object' && x !== null)) {
    throw new Error('x should be an object');
  }
  if (!('foo' in x && typeof x.foo === 'string')) {
    throw new Error('x.foo should be a string');
  }
  if (!('bar' in x && typeof x.bar === 'boolean')) {
    throw new Error('x.bar should be a boolean');
  }
  // ... a lot of validations
  return x;
};

Why not:

const guarded = guard({
  foo: string.required,
  bar: boolean.required,
  // ... other rules
})(x);

Installation

yarn add power-guard
# npm
npm i power-guard

Play with Your Favorite Framework

// Express.js
const { id, name } = guard({ id: number.required, name: string.required })(req.body);

// Koa
const { id, name } = guard({ id: number.required, name: string.required })(ctx.request.body);

Examples

Primitive Validation

import { number, string, boolean } from 'power-guard';

number.required(10); // 10
number.strict.required('10'); // error: string is not allowed in strict mode
number.gt(10).required('10'); // error: value should be greater than 10
string.with(10).required('a really long string'); // error: string has max length 10
string.optional(undefined); // undefined
boolean.required.array([true, false]); // [true, false]
boolean.required.array(true); // error: value should be an array
boolean.loose.required('1'); // true

Object Validation

import guard, { number, string, boolean } from 'power-guard';

const guardObject = guard({
  foo: number.required,
  bar: string.optional,
  baz: boolean.required,
});

const { foo, bar, baz } = guardObject({ foo: 10, baz: true }); // 10, undefined, true
const { foo, bar, baz } = guardObject({ foo: '10', baz: 'true' }); // 10, undefined, true
const { foo, bar, baz } = guardObject({ foo: 'abc', baz: 'true' }); // error: foo should be a number

Validating Complex Objects

import guard, { number, string, enumGuard, boolean, array } from 'power-guard';

enum Foo {
  key1 = 'key1',
  key2 = 2,
}

const guardObject = guard({
  foo: number.gte(10).required,
  bar: string.escaped.optional,
  baz: enumGuard(Foo).optional.array,
  bla: boolean.loose.optional,
  bnd: array(guard({ foo: number.strict.required })),
});

const guarded = guardObject({
  foo: '10',
  bar: '!abc ABC!',
  baz: [2, 'key1'],
  bla: 1,
  bnd: [{ foo: 123 }, { foo: 456 }],
});

/*
{
  foo: 10,
  bar: 'abc ABC',
  baz: [2, 'key1'],
  bla: true,
  bnd: [{ foo: 123 }, { foo: 456 }],
}
*/

Docs

The guard function

This function receives a DataConfig object as parameter, and returns a guard function which accepts an unknown variable and returns a typed object based on the config if validation succeeded, otherwise it will throw a PowerGuardError.

In short, this is the function for validating objects. Say we want an object to have the shape like:

{ foo: 123, bar: 'abc' }

Then we just need to tell power-guard:

const guardObject = guard({
  foo: number.required,
  bar: string.required,
});

You may notice there are various built-in guards, meanwhile deep customizations are also allowed:

const validate = (x: unknown): SomeType => {
  // your code goes here
};

const guardObject = guard({
  foo: validate,
});

const { foo } = guardObject(x); // foo will be `SomeType` if succeeded

Built-in Guards

Each of the guards below should follow with a .required or .optional, while the latter will allow undefined. Simply append .array in the end when needed e.g. boolean.required.array.

boolean

Boolean Guard Description
boolean A boolean value must be presented. 'true' and 'false' are acceptable.
boolean.strict A boolean value must be presented. No implicit conversions allowed.
boolean.loose A boolean value must be presented. 'true' and 'false' are acceptable. If the value can be converted to a number, then return false iff the number equals 0, otherwise true.

enum

Enum Guard Description
enumGuard(EnumType) A value of EnumType must be presented.

number

Number Guard Description
number A number value must be presented. Valid number string is also acceptable.
number.strict A number value must be presented. No implicit conversions allowed.
number.with([lower, upper]) A number value within [lower, upper] must be presented.
number.with(range: NumberRange) A number value within the range must be presented.
number.gt(x) A number value within (x, +∞) must be presented.
number.gte(x) A number value within [x, +∞) must be presented.
number.lt(x) A number value within (-∞, x) must be presented.
number.lte(x) A number value within (-∞, x] must be presented.
type NumberRange = Partial<{
  lower: number;
  equalsLower: boolean;
  upper: number;
  equalsUpper: boolean;
}>;

Note you can chain these restrictions like: number.gt(10).lte(20).strict.required

string

String Guard Description
string A string value must be presented.
string.escaped A string value must be presented. Punctuations and unknown chars will be removed and fullwidth chars will be converted into halfwidth.
string.with(x) A string value with a max length of x must be presented.

Note you can chain them as well, e.g.: string.with(10).escaped.optional

array

There's a helper function for customized array guards with optional override:

function array(elemGuard: (x: unknown) => T): (x: unknown) => Array<T>;
function array(elemGuard: (x: unknown) => T, optional: true): (x: unknown) => Array<T> | undefined;

Traditional Type Guards

power-guard will return a variable with the type you want instead of asserting types like traditional type guards. Browse types.ts if you would like to infer types.