Package Exports
- shapeit
Readme
shapeit
shapeit is an object validation tools for Javascript and, specially, Typescript. With it, you can ensure any javascript object has a provided shape corresponding to a typescript type. You can also do asynchronous data validation of any nested object and get decent error messages.
NOTE: Breaking changes on 0.5
- Importing
shapeit's subfolders won't work anymore.- Fix: If you had any import like
import { ... } from 'shapeit/...';, import it directly from'shapeit'.
- Fix: If you had any import like
Usage
shapeit consists of two different parts.
The first one is called guards and is dedicated to typecheking, so, its synchronous all the way down since typescript demands.
The second one is called validation and is dedicated to apply rules to a typechecked input. Since validation can be asynchronous, all the rules are applied asynchronously.
Guards
A basic example of guards usage would be like this
const sp = require('shapeit');
// First we create a guard
const personShape = sp.shape({
name: 'string',
age: 'number',
});
// Then we create some objects (consider they as unknown types)
const p1 = {
name: 'John',
age: 26
};
const p2 = {
name: 'Mary',
age: '27'
};
// Then we test then against the created shape
if (personShape(p1)) {
p1; // p1 is now typed as { name: string; age: number }
}
personShape.errors; // null
if (personShape(p2)) {
p2; // This line is not executed since p2 doesn't fit the shape
}
personShape.errors; // { '$.age': [ "Invalid type provided. Expected: 'number'" ] }
And a more complex one (expand)
import * as sp from 'shapeit';
const personShape = sp.shape({
name: 'string',
age: 'number',
emails: sp.arrayOf('string')
});
const person = {
name: 'John Doe',
age: 25,
emails: [
'john.doe@example.com',
'john_doe@email.com',
null
]
};
if (personShape(person)) {
person; // Unexecuted line
}
personShape.errors; // { '$.emails.2': [ "Invalid type provided. Expected: 'string'" ] }Validation
Validation can be a little bit trickier. It consists of a set of rules, which are functions that receive the object and an assert function. The trick here is that it works deeply.
A simple example would be like this
const sp = require('shapeit');
// First, we get a valid typed object
const person = {
name: 'Not John Doe',
age: 25
};
// Then we can validate it
sp.validate(person, {
name: (name, assert) => {
assert(name === 'John Doe', 'You must be John Doe');
},
age: (age, assert) => {
assert(age >= 18, 'You must be at least 18 years old');
}
}).then(result => {
result.valid; // false
result.errors; // { '$.name': ['You must be John Doe'] }
});And a more complex one (expand)
import * as sp from 'shapeit';
// Typescript interface (you can obtain that with guards too)
interface Person {
name: string;
age: number;
emails: string[];
// Notice "job" is an optional parameter
job?: {
id: number;
bossId: number;
}
}
const person: Person = {
name: 'John Doe',
age: 25,
emails: [
'john.doe@example.com',
'john_doe@email.com'
],
job: {
id: 13,
bossId: 10
}
};
validate(person, {
name: (name, assert) => {
assert(name === 'John Doe', 'You must be John Doe');
},
age: (age, assert) => {
assert(age >= 18, 'You must be at least 18 years old');
},
// An object validator can be an object with its keys
job: {
// Those rules will be evaluated only if key "job"
// exists in the person object. So, don't need to
// worry about that
id: async (jobId, assert) => {
assert(
await existsOnDb(jobId),
'This job doesnt exist on database'
)
},
// Rules can be asynchronous functions 🥳
// and all of them will be executed in parallel
bossId: async (bossId, assert) => {
assert(
await existsOnDb(bossId),
'This employee doesnt exist on database'
)
}
},
// When you need to validate the entire object and its keys,
// you can pass an array containing
// its rule and the rules for its members
emails: [
(emails, assert) => {
assert(emails.length > 0, 'Provide at least one email');
},
{
// $each is a way to apply the same rule
// to all the array elements
$each: (email, assert) => {
assert(isValidEmail(email), 'Invalid email');
}
}
]
}).then(result => {
// Do something with validation result
});This way, you can set schemas to validate all your incoming data with typesafety and get error messages matching the fields of your object.
Configuration
shapeit allows you to configure the error messages generated by the type guards like below.
const { config } = require('shapeit');
config.set('errorMessage', typename => {
return `I was expecting for a ${typename}`;
});
config.set('sizeErrorMessage', size => {
return `Give me an array of size ${size} the next time`;
});Or, if you just want to return to the default values,
config.set('errorMessage', 'default');
config.set('sizeErrorMessage', 'default');You can also get the current configuration like below
const genErrorMessage = config.get('errorMessage');Reference
Types
Primitive
String representing a JS basic type.
type Primitive =
| 'number' | 'string' | 'boolean'
| 'bigint' | 'object' | 'symbol'
| 'null' | 'undefined';
ValidationErrors
Representation of the errors found on a validation process. It's a map of property paths to an array of error messages.
type ValidationErrors = Record<string, string[]>;
Guard
Basic guard type. Can be called to verify a type synchronously. Validation errors will be present on Guard.errors after validation is complete.
type Guard<T> = {
(input: unknown): input is T;
errors: ValidationErrors;
}
GuardSchema
Schema for defining a shape guard. Represents the keys of an object mapped to their respective types.
type GuardSchema = Record<string, Primitive | Guard>;Guards
is(type: Primitive)
Creates a basic guard for a primitive type
const isString = is('string');
if (isString(value)) {
value; // string
}
else {
console.error(isString.errors); // Errors found
}
oneOf(...types: (Primitive | Guard)[])
Creates a guard for a union type from primitive names or other guards
const isValid = oneOf('string', is('number'));
if (isValid(input)) {
doSomethingWith(input); // input is typed as string | number
}
else {
console.error(isValid.errors); // Errors found
}
shape(schema: GuardSchema, strict = true)
Makes a guard for an object. Types can be specified with other guards or primitive names.
const isValidData = shape({
name: 'string',
emails: arrayOf('string')
});
if (isValidData(input)) {
doSomethingWith(input); // input is typed as { name: string, emails: string[] }
}
else {
console.error(isValidData.errors); // Errors found
}The strict parameter can be passed to specify if the validation must ensure there are no extraneous keys on the object or not (defaults to true).
const isValidData = shape({
name: 'string',
emails: arrayOf('string')
}, false);
// This will be valid
isValidData({
name: 'John Doe',
emails: ['john@doe.com', 'john.doe@example.com'],
age: 34
});
arrayOf(type: Primitive | Guard)
Creates an array shape where all elements must have the same type
const emailsShape = sp.arrayOf('string');
const peopleShape = sp.arrayOf(
sp.shape({
name: 'string',
age: 'number'
})
);
tuple(...types: (Primitive | Guard)[])
Creates a guard for a tuple type. The order of the arguments is the same as the type order of the tuple
const entryShape = sp.tuple('string', 'number');
if (entryShape(input)) {
input; // Typed as [string, number]
}
literal(...template: Template)
Creates a guard for a template literal type. It's used alongside with $ and $$.
$ is used for generating a tempate type derived from a primitive or a list of primitives or literals
const idTemplate = sp.literal('id-', sp.$('bigint'));
if (idTemplate(input)) {
input; // input is typed as `id-${bigint}`
}$$ is used for generating sets of allowed values.
const versionTemplate = sp.literal(
sp.$('bigint'), '.', sp.$('bigint'), '.', sp.$('bigint'),
sp.$$('', '-alpha', '-beta')
);
if (versionTemplate(input)) {
input; // input is typed as `${bigint}.${bigint}.${bigint}${'' | '-alpha' | '-beta'}`
}
narrow(...targets: any[])
Creates a guard that perfectly narrows a type.
const is10 = sp.narrow(10);
if (is10(input)) {
input; // typed as 10
}
const isAorB = sp.narrow('a', 'b');
if (isAorB(input)) {
input; // typed as 'a' | 'b'
}
const isLikeMyVerySpecificObject = sp.narrow({
my: {
specific: {
property: 'my-specific-value'
}
},
another: {
specific: {
property: 'another-specific-value'
}
}
});
if (isLikeMyVerySpecificObject(input)) {
input; // typed exactly as the (very specific) object provided
}
custom(name: string, validator: (input: unknown) => input is any)
Creates a custom guard from a typeguard function
const myCustomType = sp.custom(
'myCustomType',
(input): input is MyCustomType => {
let result : boolean;
// test if input is MyCustomType
return result;
}
);custom also allows you to define your own error messages by simply seting the errors property of the generated guard.
const myCustomType = sp.custom(
'myCustomType',
(input): input is MyCustomType => {
let result : boolean;
// test if input is MyCustomType
if (!result) {
myCustomType.errors = {
'$.my.property': ['This value is invalid']
}
}
return result;
}
);Helpers
partial(guard: Guard)
Creates a shape where all object keys are optional.
This is NOT valid for nested keys inside objects. If you really need it, use deepPartial instead
deepPartial(guard: Guard)
Creates a shape where all object keys and nested keys are optional
maybe(guard: Primitive | Guard)
Shorthand for oneOf(guard, 'undefined');
nullable(guard: Primitive | Guard)
Shorthand for oneOf(guard, 'null');
Roadmap
Add ESM support🎉- Improve the validation API
- Add validation mechanism to the guards API directly
- Improve docs
- Release v1.0