Validating Function Arguments in JavaScript: The Smart Way
In software engineering we try to discover and eliminate bugs as soon as possible. One of the most important heuristics here is validation of input/output on functions and methods. If you are going with TypeScript or Flow, you are fine. But if not? Then we have to manually validate at least input (arguments). What would be the best way to do it?
First comes to mind aproba. It’s ridiculously lightweight and quite popular:
import validate from "aproba";
function click( selector, x, y ) {
validate( "SNN", arguments );
}
Simple enough. For constraints you give a string where each character stands for a corresponding argument. Here we require selector to be a string (“S”) and both coordinates to be numbers (“N”).
But when it comes to optional arguments it gets quite obscure. For example, Node.js function fs.createWriteStream(path[, options]) maintainers of aproba recommend describing as SO|SS|OO|OS|S|O.
First, that is not easy to read. Second, we do not validate the structure of options – any object is considered valid.
Such limitations led me to build an alternative: byContract. It uses JSDoc expressions as constraints instead of single-character codes.
The recommended way: contract()
In v3 the cleanest approach is wrapping a function with contract(). Contracts are compiled once at definition time, so repeated calls pay no parsing cost:
import { contract, nonNull, optional, typedef } from "bycontract";
const WriteStreamOptions = typedef({
flags: "string=",
encoding: "string=",
fd: "number=",
mode: "number=",
autoClose: "boolean=",
start: "number="
});
const createWriteStream = contract(
[ "string|Buffer", optional( WriteStreamOptions ) ],
"WriteStream",
( path, options ) => {
// implementation
}
);
createWriteStream( "/tmp/out", { flags: "w" } ); // ok
createWriteStream( "/tmp/out", { flags: 42 } );
// ByContractError: createWriteStream: Argument #1: property #flags expected string but got number
Notice typedef() used here without a string name. In v3 it returns the schema object directly – no global registry, no string indirection, no ordering dependency. You just pass the returned value wherever a contract is expected.
Modifier helpers
Instead of memorising JSDoc prefix/suffix characters, v3 ships named helpers:
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 |
They return plain strings, so they compose freely:
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]
Inline validation
For arrow functions or when you want named properties in error messages, the inline style works well:
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" )
});
// ...
};
Error messages include the property name, which makes debugging faster.
JSDoc decorator
If you already write JSDoc for your classes, validateJsdoc lets you reuse it directly. Requires Babel decorators in legacy mode:
import { validateJsdoc } from "bycontract";
class Fs {
@validateJsdoc(`
/**
* @param {string|Buffer} path
* @param {string|WriteStreamOptions=} options
* @returns {WriteStream}
*/
`)
createWriteStream( path, options ) {
}
}
Exit point validation
validate() always returns the incoming value, so it can wrap a return statement directly:
function pdf() {
// ...
return validate( returnValue, "Promise" );
}
Polymorphic argument combinations
What if a function accepts different combinations of argument types? Use validateCombo:
import { validateCombo } from "bycontract";
function compare( a, b ) {
const CASE1 = [ "string", "string" ],
CASE2 = [ "string[]", "string[]" ];
validateCombo( [ a, b ], [ CASE1, CASE2 ] );
}
Throws when none of the cases match.
Reusing contracts across a codebase
One pattern that works well in larger codebases is defining constraints in a dedicated module and composing them where needed:
/interface/index.js
export const GROUP_REF = {
id: "string"
};
export const ENTITY = {
editing: "boolean=",
disabled: "boolean="
};
export const GROUP = {
title: "string=",
tests: "*[]",
...GROUP_REF
};
Then reuse them in action creators:
import { validate } from "bycontract";
import * as I from "interface";
export const addGroup = ( data, ref = null ) => ({
type: constants.ADD_GROUP,
payload: {
data: validate( data, { ...I.ENTITY, ...I.GROUP } ),
ref: validate( ref, I.GROUP_REF )
}
});
Combining abstract entity and group constraints with { ...I.ENTITY, ...I.GROUP } means adding a new entity subtype is just { ...I.ENTITY, ...I.TEST } or { ...I.ENTITY, ...I.COMMAND }.
Production
Disable validation at runtime:
import { validate, config } from "bycontract";
if ( process.env.NODE_ENV === "production" ) {
config({ enable: false });
}
Or swap the entire module with Webpack for a zero-byte production build – see the documentation for the NormalModuleReplacementPlugin setup.
Recap
byContract covers all my needs for argument validation. It is more verbose than aproba, but far more flexible. You already know the constraint syntax if you know JSDoc. v3 adds the contract() wrapper and modifier helpers, which make contracts both more ergonomic and faster at runtime (single compiled-once lookup per call). Validation can be stripped entirely from production builds.