𦩠Computed Types
Runtime validation types for TypeScript.
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.
computed-types
:
Using const UserSchema = Schema({
name: string,
amount: number,
flags: array.of(string).optional();
});
type User = Type<typeof UserSchema>;
Joi
:
Equivalent code in 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'),
);