Skip to content

neuledge/computed-types

Repository files navigation

🦩 Computed Types

Runtime validation types for TypeScript.

View On NPM Build Status Dependency Status Coverage Status License


Computed-Types (formerly: Funval) is a strongly-typed validation library for TypeScript. Using function interfaces, computed-types knows how to transform and validate your data, and automatically generates accurate TypeScript interfaces on compile time.

Using computed-types:

const UserSchema = Schema({
  name: string,
  amount: number,
  flags: array.of(string).optional();
});

type User = Type<typeof UserSchema>;

Equivalent code in Joi:

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[];
};

Main Features

  • Easy to Read - Uses runtime types like in TypeScript (including string, array, unknown, etc...)
  • Reduce Duplication - Create new validator using existing functions in seconds.
  • TypeScript Validation - Detect errors during compile time as well.
  • Function Composition - Chain multiple validators to generate new types.
  • Data Transformation - Combine validation and formatting in the one action.
  • Asynchronous & Synchronous Support - Automatically detected promises and async validation.
  • Zero Dependencies - Light and compact library.
  • Pure Javascript - Also works without TypeScript.

Sponsored by ❤️

If you like this project, please consider sponsoring us to help us continue to maintain and improve this project.


Table of Contents


Install

Node.js:

npm i computed-types

Deno:

import Schema, {
  Type,
  string,
  number,
  array,
} from 'https://denoporter.sirjosh.workers.dev/v1/deno.land/x/computed_types/src/index.ts';

Usage

import Schema, { Type, string, number, array } from 'computed-types';

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: Type '"unregistered"' is not assignable to type '"active" | "suspended"'.
  status: 'unregistered',
  items: [{ id: 'item-1', amount: 20 }],
});

console.log(err);
// 🚨 ValidationError: Expect value to equal "suspended" {
//   errors: [
//     {
//       error: TypeError: Expect value to equal "suspended",
//       path: ['status']
//     }
//   ]
// }

Creating new Types

A computed type is any function that can return a value without throwing any exceptions. Creating a custom type allows you to normalize, transform and validate any input.

For example this type will validate email addresses:

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;
}

You can use the above validator on schemas as an Email type and it will validate inputs in the form of { email: unknown } to { email: string } type.

const UserSchema = {
  email: Email,
};

const validator = Schema(UserSchema);

To create optional types, change the validator arguments to optional as well:

function OptionalEmail(input?: unknown): string | undefined {
  return input == null ? undefined : Email(input);
}

This will validate inputs in the form of { email?: unknown } to { email: string | undefined }.


Using Transform

The custom Email validator above will not support validator chaining, but we can easily fix this by using the .transform() method.

const EmailWithValidatorChain = unknown.string.transform(Email);

I can now make use of the validator chain:

const UserSchema = {
  email: EmailWithValidatorChain.optional().max(100),
};

const validator = Schema(UserSchema);

Asynchronous Validators

Asynchronous validators are supported by returning a Promise (or PromiseLike) values:

import fetch from 'node-fetch';

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;
}

Computed-types automatically detects promise and convert the return type of the Validator to promise as well:

const UserSchema = {
  username: AvailableUsername,
};
const validator = Schema(UserSchema);

const user = await validator({ username: 'test' });

Trying to access the return value without resolving it with promise first will detect and alert automatically via TypeScript on compile time.


Validators Chain

Every validator in "computed-types" is a validation function that can be called and validate any sort of data. In addition, each validator has a few helper methods to chain multiple validators together.

For example, check out this use case:

import { unknown } from 'computed-types';

const validator = unknown.number().gt(0).toFixed(2);

console.log(validator('123.4567')); // '123.46'

You can see here all the custom chain methods for each type. Please note that after calling toFixed, the validator no longer returns a number but a string so all the helpers functions available after toFixed will be the string helpers.

In addition the type helpers, each validator has those default chain helpers so use:


.equals()

Verify the return value equals to the given value.

const validator = boolean.equals(true);
.test()

Verify the return value pass the given test function.

import * as EmailValidator from 'email-validator';

const validator = string.test(EmailValidator.validate, 'Invalid email address');
.transform()

Transform the return value to a new value or throw to fail the validation process. The return value can be any value, including different types.

const validator = number.transform((x): number => {
  if (x <= 0) {
    throw new RangeError('Expected number to be positive');
  }

  return Math.sqrt(x);
});
.construct()

Similar to .transform() but less common. This helper is useful when you want to change the validator input before validating it. The returning value of the construct function should always return an array as this array will pass to the original validator input as arguments.

const validator = number.gt(1).construct((x: number, y: number) => [x + y]);
validators(x, y); // x + y
.optional()

Will convert the validator to an optional by allowing undefined or null values. This is very useful for parsing when creating optional properties on a schema.

const validator = Schema({
  name: string.trim().min(1),
  address: string.trim().optional(),
});
.strictOptional()

Same as .optional() but allows only undefined values.

const validator = Schema({
  name: string.trim().min(1),
  address: string.trim().optional(),
});
.destruct()

Use this as the final helper on the chain. It will catch any validation error and spread it to a 2-arguments array with an error and possible value on success. Useful if you don't like catching errors.

const validator = Schema({
  name: string.trim().min(1),
}).destruct();

const [err, user] = validator(req.body);
.error()

Will catch any error and replace it with your custom error instead. You can pass a string, ValidationError or a function that will generate an error for you. Notice that on most cases you will not need to use this helpers, as most validation helpers has an optional error param with the same functionality.

const validator = Schema({
  name: string.error('expect input to be string'),
  amount: number.gt(0, (val) => `${val} is not positive amount`);
});

Available Types

It's useful to import the following native types when building custom schemas. Click on each type to see some validation examples.

import Schema, { unknown, string, number, boolean, array, DateType } from 'computed-types';


Schema

Create a validator from schema object, values or function validators.

const validator = Schema(
  {
    name: string,
    amount: number,
  },
  'Missing name or amount',
);
Strict mode

By default, the schema validator will ignore all properties that aren't exist on the schema. If you want to throw an error instead you can toggle the strict mode on.

const validator = Schema(
  {
    name: string,
    amount: number,
  },
  { strict: true },
);
Schema.either

Works as OR switch. Create a validator from multiple function validators or schema objects.

const validator = Schema.either({ foo: string }, { bar: number });
// validate: { foo: string; } | { bar: number; }
Schema.merge

Works as AND switch. Create a validator from multiple function validators or schema objects.

const validator = Schema.merge({ foo: string }, { bar: number });
// validate: {
//   foo: string;
//   bar: number;
// }
Schema.enum

Create a validator from TypeScript enum.

enum Status {
  OK,
  Invalid,
}

const validator = Schema.enum(Status, 'Invalid status');
Schema.record

Create a Record<key, value> validator.

const validator = Schema.record(string.regexp(/^[a-z]+$/), number);

unknown

Accept any unknown value:

const validator = Schema({
  data: unknown,
});
unknown.schema()

Accept any value as an input and try to convert it the given schema:

const validator = unknown.schema({
  foo: string.trim(),
});
unknown.object()

Accept any value as an input and try to convert it to an object:

const validator = unknown.object('Expect data to be an object');
unknown.array()

Accept any value as an input and try to convert it to an array:

const validator = unknown.array().min(1).of(boolean);
unknown.string()

Accept any value as an input and try to convert it to a string:

const validator = unknown.string('Expect data to be string').toUpperCase();

// will accept: `{ data: 1 }` and convert it to `{ data: '1' }`
// will throw: `{ data: null }`
unknown.number()

Accept any value as an input and try to convert it to a number:

const validator = unknown.number('Expect data to be number').gt(0);
unknown.boolean()

Accept any value as an input and try to convert it to a boolean:

const validator = unknown.boolean('Expect data to be boolean').equals(true);
unknown.date()

Accept any value as an input and try to convert it to a date:

const validator = unknown
  .date('Expect data to be date')
  .equals('1970-01-01T00:00:00.050Z');
unknown.enum()

Accept any value as an input and try to convert it to the given enum:

enum Status {
  OK,
  Invalid,
}

const validator = unknown.enum(Status);
unknown.record()

Accept any value as an input and try to convert it to a Record<key, value>:

const validator = unknown.record(string, number);

string

Accept only string values (including empty strings).

const validator = Schema({
  content: string,
});
string.toLowerCase()

Accept string and convert it to lower case.

const validator = string.toLowerCase().trim();
string.toUpperCase()

Accept string and convert it to upper case.

const validator = string.toUpperCase().trim();
string.toLocaleLowerCase()

Accept string and convert it to local lower case.

const validator = string.toLocaleLowerCase('en-US').trim();
string.toLocaleUpperCase()

Accept string and convert it to local upper case.

const validator = string.toLocaleUpperCase('en-US').trim();
string.trim()

Accept string and trim it.

const validator = string.trim();
string.truncate()

Truncate a string to a given length with ellipsis () to the end. If the string below the given limit the original string is return.

const validator = string.truncate(100);
string.normalize()

Accept string and normalize it.

const validator = string.normalize();
string.min()

Accept string with minimum given length.

const validator = string.min(2).toLowerCase();
string.max()

Accept string with maximum given length.

const validator = string.max(10).toUpperCase();
string.between()

Accept string within the given length range.

const validator = string.between(2, 10).trim();
string.regexp()

Accept only strings that match the given regular expression.

const validator = string.regexp(/^Hello/).trim();

number

Accept only number type values.

const validator = Schema({
  amount: number,
});
number.float()

Accept only floating numbers (throws on NaN or non-finite values).

const validator = number.float().gt(0);
number.integer()

Accept only integer numbers.

const validator = number.integer().gt(0);
number.toExponential()

Accept number and convert it to exponential format string.

const validator = number.toExponential().toUpperCase();
number.toFixed()

Accept number and convert it to fixed format string.

const validator = number.toFixed(3);
number.toLocaleString()

Accept number and convert it to locale string.

const validator = number.toLocaleString('en-US');
number.toPrecision()

Accept number and convert it to precision string.

const validator = number.toPrecision(2);
number.toString()

Accept number and convert it to string.

const validator = number.toString(16).toUpperCase();
number.gte()

Accept number that greater or equal than the boundary given.

const validator = number.gte(1.5);
number.lte()

Accept number that lower or equal than the boundary given.

const validator = number.lte(10.5);
number.gt()

Accept number that greater than the boundary given.

const validator = number.gt(1.5);
number.lt()

Accept number that lower than the boundary given.

const validator = number.lt(10.5);
number.between()

Accept number between the given boundaries.

const validator = number.between(0, 1);

boolean

Accept only boolean type values.

const validator = Schema({
  agree: boolean,
});

array

Accept only array type values.

const validator = Schema({
  agree: array
});

##### `array.of()`

Accept only array with given items.

```ts
const numbers = array.of(number); // numbers[]
const tuple = array.of(number).between(1, 2); // [number, number?]
const objects = array.of({ foo: number }); // { foo: number }[]
const enums = array.of(Schema.enum(Status); // Status[]
array.min()

Accept only array with minimum given items.

const validator = array.min(2);
array.max()

Accept only array with maximum given items.

const validator = array.max(10);
array.between()

Accept only array with minimum and maximum count of items.

const validator = array.between(2, 10);

DateType

Accept only instances of Date.

const validator = Schema({
  eventTime: DateType,
});
DateType.toISOString()

Accept Date and convert it to ISO date string.

const validator = DateType.toISOString();
DateType.getTime()

Accept Date and convert it to a timestamp.

const validator = DateType.getTime().gt(100);
DateType.gte()

Accept Date that greater or equal than the boundary given.

const validator = DateType.gte(new Date('2020-10-01T10:00:00.000Z'));
DateType.lte()

Accept Date that lower or equal than the boundary given.

const validator = DateType.lte(new Date('2020-10-01T10:00:00.000Z'));
DateType.gt()

Accept Date that greater than the boundary given.

const validator = DateType.gt(new Date('2020-10-01T10:00:00.000Z'));
DateType.lt()

Accept Date that lower than the boundary given.

const validator = DateType.lt(new Date('2020-10-01T10:00:00.000Z'));
DateType.between()

Accept Date between the given boundaries.

const validator = DateType.between(
  new Date('2020-09-01T10:00:00.000Z'),
  new Date('2020-10-01T10:00:00.000Z'),
);

License

MIT license © 2020 Neuledge