Runtime type checking for JavaScript and TypeScript. Declare contracts with JSDoc syntax. No transpilation required, zero production overhead.
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.
- Quick start
- Installation
- Modifier helpers
- Function contracts
- Types
- Custom types
- Custom validators
- Exceptions
- Combinations
- Production
ByContract supports several styles of runtime validation. Most projects will use either contract() or validate() with named helper functions.
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 stringA 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" )
});
// ...
};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=" ]
);
}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 }]
]
}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}
`;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;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]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 string*, 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 nullvalidate( 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 arrayfunction 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 numbervalidate( 100, "?number" ); // ok
validate( null, "?number" ); // okvalidate( 42, "!number" ); // ok
validate( null, "!number" ); // ByContractError: expected non-nullable but got nullvalidate( [ 1, 2 ], "number[]" ); // ok
validate( [ 1, "x" ], "number[]" ); // ByContractError: array element 1: expected number but got string
validate( [ 1, 2 ], "Array.<number>" ); // ok — JSDoc syntaxvalidate( { a: "foo", b: "bar" }, "Object.<string, string>" ); // ok
validate( { a: "foo", b: 100 }, "Object.<string, string>" );
// ByContractError: object property b: expected string but got numbervalidate( { 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 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>" ); // oktypedef( 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 );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 nullExtend the is object with custom predicates:
import { validate, is } from "bycontract";
is.email = ( val ) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( val );
validate( "me@dsheiko.com", "email" ); // ok
validate( "not-an-email", "email" ); // ByContractError: expected email but got stringEvery 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"
}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.
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