npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

shapeit

v0.10.0

Published

Object validation tools for Javascript and, specially, Typescript

Downloads

26

Readme

shapeit

GitHub license npm version Github Actions codecov

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

from 0.6.x to 0.7

  • assert function from validate now throws an error instead of returning the original contition.
    • Fix: If you have any rule that uses assert two or more times sequentially, convert it to an array of rules.
sp.validate(myObj, {
    myProperty: (value, assert) => {
        assert(testCondition1(value), 'error message 1');
        assert(testCondition2(value), 'error message 2');
    }
});

would be replaced by

sp.validate(myObj, {
    myProperty: [
        (value, assert) => assert(testCondition1(value), 'error message 1'),
        (value, assert) => assert(testCondition2(value), 'error message 2'),
    ]
});

or simply

sp.validate(myObj, {
    myProperty: [
        value => sp.assert(testCondition1(value), 'error message 1'),
        value => sp.assert(testCondition2(value), 'error message 2'),
    ]
});

from 0.4.x to 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'.

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'" ] }

// Or, for a list of all errors

personShape.errors.all // [{ path: '$.age', message: "Invalid type provided. Expected: 'number'" }]
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: [
        '[email protected]',
        '[email protected]',
        null
    ]
};

if (personShape(person)) {
    person; // Unexecuted line
}

personShape.errors; // { '$.emails.2': [ "Invalid type provided. Expected: 'string'" ] }

Validation

Validation consists of matching an object against a set of rules.

A set of rules can be either

  • A validation function that receives the original object and the assert function
  • A validation object containing some of the input's keys, each one matching a set of rules for the corresponding key on the input
  • An array containing multiple sets of rules

Any failing assertion will throw an AssertionError

A simple example looks 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 can be imported directly from shapeit.
        // The second argument is optional
        sp.assert(age >= 18, 'You must be at least 18 years old');
    }
}).then(result => {
    result.valid; // false
    result.errors; // { '$.name': ['You must be John Doe'] }
});
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: [
        '[email protected]',
        '[email protected]'
    ],
    job: {
        id: 13,
        bossId: 10
    }
};

sp.validate(person, {
    name: name => {
        sp.assert(name === 'John Doe', 'You must be John Doe');
    },
    age: age => {
        sp.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 => {
            sp.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 => {
            sp.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 => {
            sp.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 => {
                sp.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

String representing a JS basic type.

type Primitive =
    | 'number' | 'string'    | 'boolean'
    | 'bigint' | 'object'    | 'symbol'
    | 'null'   | 'undefined';

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

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

Schema for defining a shape guard. Represents the keys of an object mapped to their respective types.

type GuardSchema = Record<string, Primitive | Guard>;

Guards

Creates a basic guard for a primitive type

const isString = is('string');

if (isString(value)) {
    value; // string
}
else {
    console.error(isString.errors); // Errors found
}

Creates a native instanceof guard. Can be useful when used in conjunction with other guards.

class MyClass {
  // My class code...
}

const isMyClass = instanceOf(MyClass);

if (isMyClass(value)) {
    doSomethingWith(value); // value is typed as MyClass
}
else {
    console.error(isMyClass.errors); // Errors found
}

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
}

Creates a guard for a intersection type from primitive names or other guards

const isValid = allOf(
    looseShape({ a: 'string' }),
    looseShape({ b: 'number' })
);

if (isValid(input)) {
    doSomethingWith(input); // input is typed as { a: string; b: number; }
}
else {
    console.error(isValid.errors); // Errors found
}

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: ['[email protected]', '[email protected]'],
  age: 34
});
const emailsShape = sp.arrayOf('string');

const peopleShape = sp.arrayOf(
  sp.shape({
    name: 'string',
    age: 'number'
  })
);

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

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'}`
}

$$$ is used for generating recursive template types

const testTemplate = sp.literal(
    'a-', sp.$('number', sp.$$$('b', $('number')))
);

if (testTemplate(input)) {
    input; // input is typed as `a-${number | `b-${number}`}`;
}

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
}

Creates a guard that always validates

Equivalent to unknown type in TS.

Equivalent to never type in TS.

Creates a guard that always validates

Equivalent to any type in TS.

For improved type safety, use unknown instead

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

This is NOT valid for nested keys inside objects. If you really need it, use deepPartial instead

Roadmap

  • ~~Add ESM support~~ 🎉
  • ~~Improve the validation API~~ 🎉
  • ~~Add validation mechanism to the guards API directly~~ 🎉
  • Improve docs
  • Release v1.0