Argument validation library based on JSDOC syntax
bycontract
– A lightweight runtime type checker for JavaScript and TypeScript, powered by JSDOC syntax. Ensure your functions receive the right arguments with simple, declarative contract definitions. No transpilation needed—just clean, reliable validation.
function pdf( path, w, h, options, callback ) {
validate( arguments, [
"string",
"!number",
"!number",
PdfOptionsType,
"function=" ] );
}
function pdf( path, w, h, options, callback ) {
validateContract`
{string} ${ path }
{!number} ${ w }
{!number} ${ h }
{#PdfOptionsType} ${ options }
{function=} ${ callback }
`;
}
class Page {
@validateJsdoc(`
@param {string} path
@param {!number} w
@param {!number} h
@param {#PdfOptionsType} options
@param {function=} callback
@returns {Promise}
`)
pdf( path, w, h, options, callback ) {
return Promise.resolve();
}
}
npm install bycontract
const { validate } = require( "bycontract" );
validate( 1, "number|string" );
<script src="dist/byContract.min.js"></script>
<script>
const { validate } = byContract;
validate( 1, "number|string" );
</script>
npm install bycontract
import { validate } from "bycontract";
validate( 1, "number|string" );
validate( arguments, [ "JSDOC-EXPRESSION", "JSDOC-EXPRESSION" ] ); // ok or exception
validate( value, "JSDOC-EXPRESSION" ); // ok or exception
import { validate } from "bycontract";
const PdfOptionsType = {
scale: "?number"
}
/**
* Example
* @param {string} path
* @param {!number} w
* @param {!number} h
* @param {PdfOptionsType} options
* @param {function=} callback
*/
function pdf( path, w, h, options, callback ) {
validate( arguments, [
"string",
"!number",
"!number",
PdfOptionsType,
"function=" ] );
//...
const returnValue = Promise.resolve();
return validate( returnValue, "Promise" );
}
pdf( "/tmp/test.pdf", 1, 1, { scale: 1 } );
// Test it
pdf( "/tmp/test.pdf", "1", 1, { scale: 1 } ); // ByContractError: Argument #1: expected non-nullable but got string
validateContract`
{JSDOC-EXPRESSION} ${ var1 }
{JSDOC-EXPRESSION} ${ var2 }
`;
import { validate, typedef } from "bycontract";
typedef("#PdfOptionsType", {
scale: "number"
});
function pdf( path, w, h, options, callback ) {
validateContract`
{string} ${ path }
{!number} ${ w }
{!number} ${ h }
{#PdfOptionsType} ${ options }
{function=} ${ callback }
`;
}
or you can copy/paste from JSDoc:
function pdf( path, w, h, options, callback ) {
validateContract`
@param {string} ${ path }
@param {!number} ${ w }
@param {!number} ${ h }
@param {#PdfOptionsType} ${ options }
@param {function=} ${ callback }
`;
}
@validateJsdoc`
@param {JSDOC-EXPRESSION} param1
@param {JSDOC-EXPRESSION} param2
`;
import { validate, typedef } from "bycontract";
typedef("#PdfOptionsType", {
scale: "number"
});
class Page {
@validateJsdoc(`
@param {string} path
@param {!number} w
@param {!number} h
@param {#PdfOptionsType} options
@param {function=} callback
@returns {Promise}
`)
pdf( path, w, h, options, callback ) {
return Promise.resolve();
}
}
const page = new Page();
page.pdf( "/tmp/test.pdf", "1", 1, { scale: 1 } );
// ByContractError:
// Method: pdf, parameter w: expected non-nullable but got string
This solution requires legacy decorators proposal support. You can get it with following Babel configuration
{
presets: [
[ "@babel/preset-env" ]
],
plugins: [
[ "@babel/plugin-proposal-decorators", { "legacy": true } ]
]
}
You can use one of primitive types: *
, array
, string
, undefined
, boolean
, function
, nan
, null
, number
, object
, regexp
validate( true, "boolean" );
// or
validate( true, "Boolean" );
validate( null, "boolean" ); // ByContractError: expected boolean but got null
const fn = () => validate( arguments, [ "boolean", "*" ]);
fn( null, "any" ); // ByContractError: Argument #0: expected boolean but got null
validate( 100, "string|number|boolean" ); // ok
validate( "foo", "string|number|boolean" ); // ok
validate( true, "string|number|boolean" ); // ok
validate( [], "string|number|boolean" );
// ByContractError: expected string|number|boolean but failed on each:
// expected string but got array, expected number but got array, expected boolean but got array
function foo( bar, baz ) {
validate( arguments, [ "number=", "string=" ] );
}
foo(); // ok
foo( 100 ); // ok
foo( 100, "baz" ); // ok
foo( 100, 100 ); // ByContractError: Argument #1: expected string but got number
foo( "bar", "baz" ); // ByContractError: Argument #0: expected number but got string
validate( [ 1, 1 ], "Array.<number>" ); // ok
validate( [ 1, "1" ], "Array.<number>" );
// ByContractError: array element 1: expected number but got string
// or
validate( [ 1, 1 ], "number[]" ); // ok
validate( [ 1, "1" ], "number[]" );
// ByContractError: array element 1: expected number but got string
validate( { foo: "foo", bar: "bar" }, "Object.<string, string>" ); // ok
validate( { foo: "foo", bar: 100 }, "Object.<string, string>" );
// ByContractError: object property bar: expected string but got number
validate({
foo: "foo",
bar: 10
}, {
foo: "string",
bar: "number"
}); // ok
validate({
foo: "foo",
bar: {
quiz: [10]
}
}, {
foo: "string",
bar: {
quiz: "number[]"
}
}); // ok
validate({
foo: "foo",
bar: 10
}, {
foo: "string",
bar: "number"
}); // ByContractError: property #bar expected number but got null
You can validate if a supplied value is an instance of a declared interface:
class MyClass {}
const instance = new MyClass();
validate( instance, MyClass ); // ok
class MyClass {}
class Bar {}
const instance = new MyClass();
validate( instance, Bar );
// ByContractError: expected instance of Bar but got instance of MyClass
When the interface is globally available you can set contract as a string:
const instance = new Date();
validate( instance, "Date" ); // ok
//..
validate( node, "HTMLElement" ); // ok
//..
validate( ev, "Event" ); // ok
Globally available interfaces can also be used in Array/Object expressions:
validate( [ new Date(), new Date(), new Date() ], "Array.<Date>" ); // ok
validate( 100, "?number" ); // ok
validate( null, "?number" ); // ok
import { validate, Exception } from "bycontract";
try {
validate( 1, "NaN" );
} catch( err ) {
console.log( err instanceof Error ); // true
console.log( err instanceof TypeError ); // true
console.log( err instanceof Exception ); // true
console.log( err.name ); // ByContractError
console.log( err.message ); // expected nan but got number
}
Sometimes we allow function to accept different sequences of types.
Let’s take an example:
function andLogAndFinish( spec, tracker, done ) {
validate( "SOF|SZF|OOF|OZF", [ spec, tracker, done ] )
//...
}
Where the following sequences of types valid:
import { validateCombo } from "bycontract";
const CASE1 = [ "string", TRACKER_OPTIONS, "function" ],
CASE2 = [ "string", null, "function" ],
CASE3 = [ SPEC_OPTIONS, TRACKER_OPTIONS, "function" ],
CASE4 = [ SPEC_OPTIONS, null, "function" ];
validateCombo( arguments, [ CASE1, CASE2, CASE3, CASE4 ] );
Function validateCombo
throws exception when none of the cases is valid
Pretty much like with JSDoc @typedef one can declare a custom type and use it as a contract.
Here we define a union type for values that can contain either numbers or strings that represent numbers.
import { validate, typedef } from "bycontract";
typedef( "NumberLike", "number|string" );
validate( 10, "NumberLike" ); // OK
validate( null, "NumberLike" ); // ByContractError: expected number|string but got null
This example defines a type Hero
that represents an object/namespace required to have properties hasSuperhumanStrength
and hasWaterbreathing
both of boolean type.
import { validate, typedef } from "bycontract";
typedef( "#Hero", {
hasSuperhumanStrength: "boolean",
hasWaterbreathing: "boolean"
});
var superman = {
hasSuperhumanStrength: true,
hasWaterbreathing: false
};
validate( superman, "#Hero" ); // OK
When any of properties violates the specified contract an exception thrown
var superman = {
hasSuperhumanStrength: 42,
hasWaterbreathing: null
};
validate( superman, "#Hero" ); // ByContractError: property #hasSuperhumanStrength expected boolean but got number
If value misses a property of the complex type an exception thrown
var auqaman = {
hasWaterbreathing: true
};
validate( superman, "#Hero" ); // ByContractError: missing required property #hasSuperhumanStrength
Basic type validators exposed exported as is
object. So you can extend it:
import { validate, is } from "bycontract";
is.email = function( val ){
var re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test( val );
}
validate( "me@dsheiko.com", "email" ); // ok
validate( "bla-bla", "email" ); // ByContractError: expected email but got string
You can disable validation logic for production env like
import { validate, config } from "bycontract";
if ( process.env.NODE_ENV === "production" ) {
config({ enable: false });
}
Alternatively you can fully remove the library from the production codebase with Webpack:
const webpack = require( "webpack" ),
TerserPlugin = require( "terser-webpack-plugin" );
module.exports = {
mode: process.env.NODE_ENV || "development",
...
optimization: {
minimizer: [
new TerserPlugin(),
new webpack.NormalModuleReplacementPlugin(
/dist\/bycontract\.dev\.js/,
".\/bycontract.prod.js"
)
]
}
};
npx NODE_ENV=development webpack
npx NODE_ENV=production webpack