bycontract
v3.0.1
Published
argument validation library based on JSDOC syntax
Maintainers
Readme
ByContract 3
Runtime type checking for JavaScript and TypeScript. Declare contracts with JSDoc syntax. No transpilation required, zero production overhead.
Why ByContract?
TypeScript catches type errors at compile time. ByContract validates values at runtime.
This is useful when dealing with:
- API payloads
- User input
- Environment variables
- Third-party libraries
- Untyped JavaScript consumers
Contracts can be enabled during development and removed entirely from production builds.
Contents
- Quick start
- Installation
- Modifier helpers
- Function contracts
- Types
- Custom types
- Custom validators
- Exceptions
- Combinations
- Production
Quick start
ByContract supports several styles of runtime validation. Most projects will use either contract() or validate() with named helper functions.
Function contracts (recommended)
Wrap a function with parameter and return-value contracts. Contracts are compiled once and cached at definition time.
import { contract, nonNull, optional, typedef } from "bycontract";
const PdfOptionsType = typedef({
scale: "?number"
});
const pdf = contract(
[
"string",
nonNull( "number" ),
nonNull( "number" ),
PdfOptionsType,
optional( "function" )
],
"Promise",
( path, w, h, options, callback ) => {
return generatePdf( path, w, h, options ).then( callback );
}
);
pdf( "/tmp/out.pdf", 210, 297, { scale: 2 } ); // ok
pdf( "/tmp/out.pdf", "210", 297, { scale: 2 } );
// ByContractError: pdf: Argument #1: expected non-nullable but got stringInline validation
A good fit for arrow functions. Property names are included in validation errors.
import { validate, nonNull, optional } from "bycontract";
const pdf = ( path, w, h, options, callback ) => {
validate( { path, w, h, options, callback }, {
path: "string",
w: nonNull( "number" ),
h: nonNull( "number" ),
options: { scale: "?number" },
callback: optional( "function" )
});
// ...
};JSDoc syntax
For projects already using JSDoc-style contracts.
import { validate } from "bycontract";
function pdf( path, w, h, options, callback ) {
validate(
arguments,
[ "string", "!number", "!number", PdfOptionsType, "function=" ]
);
}Decorator flavor
Requires Babel decorators in legacy mode.
import { validateJsdoc } from "bycontract";
class Page {
@validateJsdoc(`
@param {string} path
@param {!number} w
@param {!number} h
@returns {Promise}
`)
pdf( path, w, h ) {
return Promise.resolve();
}
}
new Page().pdf( "/tmp/test.pdf", "not-a-number", 297 );
// ByContractError: Method: pdf, parameter w: expected non-nullable but got stringConfiguration:
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }]
]
}Template literal flavor
For projects that prefer inline JSDoc-style validation without decorators.
import { validateContract } from "bycontract";
const path = "/tmp/out.pdf";
const width = 210;
validateContract`
@param {string} ${path}
@param {!number} ${width}
`;Installation
npm install bycontract// CommonJS
const { validate } = require( "bycontract" );
// ES module
import { validate } from "bycontract";
// Browser
// <script src="dist/byContract.min.js"></script>
// const { validate } = byContract;Modifier helpers
Import named helpers instead of memorising JSDoc prefix/suffix characters:
import { optional, nullable, nonNull, arrayOf, union } from "bycontract";| Helper | JSDoc equivalent | Meaning |
|--------|-----------------|---------|
| optional("number") | "number=" | Parameter may be omitted |
| nullable("number") | "?number" | Value may be null |
| nonNull("number") | "!number" | Rejects null and undefined |
| arrayOf("string") | "string[]" | Every element must match the type |
| union("number","string") | "number\|string" | Accepts any listed type |
Helpers return plain strings, so they compose freely with any contract position:
validate( { name, age, role }, {
name: nonNull( "string" ),
age: nonNull( "number" ),
role: optional( union( "string", "null" ) )
});
validate( ids, arrayOf( "number" ) );
validate( scores, arrayOf( nonNull( "number" ) ) ); // rejects [1, null, 3]Function contracts
contract( paramContracts, fn ) or contract( paramContracts, returnContract, fn ).
Works with arrow functions. Contracts are pre-compiled at definition time.
import { contract, nonNull, optional } from "bycontract";
// Positional array contracts
const add = contract( [ "number", "number" ], ( a, b ) => a + b );
add( 1, 2 ); // 3
add( 1, "two" ); // ByContractError: add: Argument #1: expected number but got string
// With return-type validation
const parseId = contract( [ "string" ], "number", str => parseInt( str, 10 ) );
// Named-param schema — single destructured argument, best error messages
const render = contract(
{ path: "string", w: nonNull( "number" ), callback: optional( "function" ) },
( { path, w, callback } ) => { /* … */ }
);
render( { path: "/", w: "oops" } );
// ByContractError: render: property #w expected non-nullable but got stringTypes
Primitives
*, array, boolean, function, nan, null, number, object, regexp, string, undefined
validate( true, "boolean" ); // ok
validate( true, "Boolean" ); // ok — case-insensitive
validate( null, "boolean" ); // ByContractError: expected boolean but got nullUnion
validate( 100, "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 arrayOptional
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 numberNullable
validate( 100, "?number" ); // ok
validate( null, "?number" ); // okNon-nullable
validate( 42, "!number" ); // ok
validate( null, "!number" ); // ByContractError: expected non-nullable but got nullTyped arrays
validate( [ 1, 2 ], "number[]" ); // ok
validate( [ 1, "x" ], "number[]" ); // ByContractError: array element 1: expected number but got string
validate( [ 1, 2 ], "Array.<number>" ); // ok — JSDoc syntaxTyped objects
validate( { a: "foo", b: "bar" }, "Object.<string, string>" ); // ok
validate( { a: "foo", b: 100 }, "Object.<string, string>" );
// ByContractError: object property b: expected string but got numberObject schema
validate( { foo: "foo", bar: 10 }, { foo: "string", bar: "number" } ); // ok
validate( { foo: "foo", bar: { quiz: [10] } }, {
foo: "string",
bar: { quiz: "number[]" }
}); // ok — nested schemas work recursivelyClass / interface
class MyClass {}
validate( new MyClass(), MyClass ); // ok
validate( new MyClass(), Bar ); // ByContractError: expected instance of Bar but got instance of MyClass
// Global interfaces by string
validate( new Date(), "Date" ); // ok
validate( node, "HTMLElement" ); // ok
validate( [ new Date() ], "Array.<Date>" ); // okCustom types
Value-based (recommended)
typedef( schema ) returns the schema directly — no global registry, no string indirection:
import { validate, typedef, contract } from "bycontract";
const HeroType = typedef({
hasSuperhumanStrength: "boolean",
hasWaterbreathing: "boolean"
});
validate( superman, HeroType );
const createHero = contract( [ HeroType ], hero => hero );String registry (legacy)
import { validate, typedef } from "bycontract";
typedef( "#Hero", {
hasSuperhumanStrength: "boolean",
hasWaterbreathing: "boolean"
});
validate( superman, "#Hero" ); // ok
validate( { hasSuperhumanStrength: 42, hasWaterbreathing: null }, "#Hero" );
// ByContractError: property #hasSuperhumanStrength expected boolean but got number
validate( { hasWaterbreathing: true }, "#Hero" );
// ByContractError: missing required property #hasSuperhumanStrengthUnion typedef:
typedef( "NumberLike", "number|string" );
validate( 10, "NumberLike" ); // ok
validate( null, "NumberLike" ); // ByContractError: expected number|string but got nullCustom validators
Extend the is object with custom predicates:
import { validate, is } from "bycontract";
is.email = ( val ) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( val );
validate( "[email protected]", "email" ); // ok
validate( "not-an-email", "email" ); // ByContractError: expected email but got stringExceptions
Every validation failure throws a ByContractError (extends TypeError):
import { validate, Exception } from "bycontract";
try {
validate( 1, "NaN" );
} catch ( err ) {
err instanceof Error; // true
err instanceof TypeError; // true
err instanceof Exception; // true
err.name; // "ByContractError"
err.message; // "expected nan but got number"
err.code; // "EINVALIDTYPE"
}Combinations
Validate functions that accept several distinct argument signatures:
import { validateCombo } from "bycontract";
const CASE1 = [ "string", TrackerOptions, "function" ];
const CASE2 = [ "string", null, "function" ];
const CASE3 = [ SpecOptions, TrackerOptions, "function" ];
const CASE4 = [ SpecOptions, null, "function" ];
function andLogAndFinish( spec, tracker, done ) {
validateCombo( [ spec, tracker, done ], [ CASE1, CASE2, CASE3, CASE4 ] );
}Throws when none of the cases match.
Production
Disable at runtime:
import { validate, config } from "bycontract";
if ( process.env.NODE_ENV === "production" ) {
config({ enable: false });
}Or swap the entire module with Webpack (zero-byte production build):
// webpack.config.js
const webpack = require( "webpack" );
const 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"
)
]
}
};NODE_ENV=development npx webpack # includes validation
NODE_ENV=production npx webpack # strips validation entirely
