Guarding Functions in JavaScript
Having the ability to detect and report errors early on in the lifecycle of an error is key to debugging, especially in JavaScript. Read on for some code and how to make this happen easily.
Join the DZone community and get the full member experience.
Join For FreeAs developers, we spend a lot of our time on debugging and particularly on spotting the source of a problem. DevTools guides us though the call stack, but the tracing process can be still pretty time consuming, especially on a cascade of asynchronous calls. The remedy here is early problem reporting.
Let's say we have a function to search trough a multidimensional structure for the elements containing a given string. We make a call that looks like legit:
grep( "substring", tree );
Yet we don't get the expected result. We would spend some time on examining the given tree structure and it can be quite a big one. Then we would probably do other checks, but eventually, we would find out from the code of the function that it expects the arguments in the opposite order. Thus if we had just guarded the function parameters, we would not lose all of this time:
function grep( tree, substring ){
if ( !( tree instanceof Tree ) ) {
throw TypeError( "Invalid tree parameter" );
}
if ( typeof substring !== "string" ) {
throw TypeError( "Invalid substring parameter" );
}
//...
}
This kind of validation is a part of Design by Contract approach . It states for validation of preconditions and postconditions within a software component. In our case, we have to test our function input against a specified contract (tree is an instance of Tree and substring is a string) and advisable we check the output to be a string.
Yeah, JavaScript doesn't have currently built-in facilities for entry/endpoint validation like other languages. For an instance PHP has type hinting:
<?php
function grep( Tree $tree, string $substring ): string {}
TypeScript has strict types:
function grep( tree: Tree, substring: string ): string {}
In addition, it supports also advanced types (union, optional, intersection, generics and others):
function normalize( numberLike: number | string, modifier?: boolean ): string {}
Among the features proposed for ES. Next, there is one called Guards that suggests the following syntax:
function grep( tree:: Tree, substring:: String ):: String {}
Nowadays in JavaScript, we have to cope with external libraries or transpilers. However, just a few can be found. One of the oldest libraries is Cerny.js . It is very much of DbC, powerful and flexible:
var NewMath = {};
(function() {
var check = CERNY.check;
var pre = CERNY.pre;
var method = CERNY.method;
// The new division
function divide(a,b) {
return a / b;
}
method(NewMath, "divide", divide);
// The precondition for a division
pre(divide, function(a,b) {
check(b !== 0, "b may not be 0");
});
})();
But as for me, it's too complex to read. I would prefer something concise and clean just to test pre-/postcoditions. The syntax provided by Contractual is very much of what I mean:
function divide ( a, b ) {
pre:
typeof a === "number";
typeof b === "number";
b !== 0, "May not divide by zero";
main:
return a / b;
post:
__result < a;
}
alert(divide(10, 0));
Everything looks fine except it's no JavaScript. One has to compile the sources to JavaScript with Contractual or Babel Contracts. I have nothing against transpilers, but if to use one, I would rather go with TypeScript.
But coming back to JavaScript, have you every realized that regardless of libraries and frameworks we keep already declaring entry/exit point contracts when commenting functions and classes with JSDoc. It would be just perfect if doc comments were used for validation. As you understand, it cannot be done without compilation. However, we can use a library that relies on JSDoc expressions. Fortunately, that's exactly what byContract does. Here what the syntax look like:
/**
* @param {number|string} sum
* @param {Object.<string, string>} dictionary
* @param {function} transformer
* @returns {HTMLElement}
*/
function makeTotalElement( sum, dictionary, transformer ) {
// Test if the contract is respected at entry point
byContract( arguments, [ "number|string", "Object.<string, string>", "function" ] );
// ..
var res = document.createElement( "div" );
// ..
// Test if the contract is respected at exit point
return byContract( res, "HTMLElement" );
}
// Test it
var el1 = makeTotalElement( 100, { foo: "foo" }, function(){}); // ok
var el2 = makeTotalElement( 100, { foo: 100 }, function(){}); // exception
As you see we can copy/paste types from the doc comment to byContract and that makes a contract, that simple. Let's examine it more closely. byContract can be accessed as a UMD module (both AMD/CommonJS-compliant) or as a global variable. We can pass to it either value/JSDoc expression pair
byContract( value, "JSDOC-EXPRESSION" );
or list of values against a list of expressions:
byContract( [ value, value ], [ "JSDOC-EXPRESSION", "JSDOC-EXPRESSION" ] );
byContract tests the values and if the associated contract (as JSDoc expression) violated it throws byContract.Exception (which is an instance of TypeError) with a message like `Value violates the contract NaN`.
In the simplest case the contract is set to validate against a primitive type like `array`, `string`, `undefined`, `boolean`, `function`, `nan`, `null`, `number`, `object`, `regexp`:
byContract( true, "boolean" );
When we need to allow value to be one of a list of specified types we can use type union.
byContract( 100, "string|number|boolean" );
A function can have mandatory as well as optional parameters. By default, a parameter provided with a primitive type in the contract is considered mandatory. But with '=' modifier we can set it as optional. So byContract that treats e.g. `number=` like `number|undefined`
function foo( bar, baz ) {
byContract( arguments, [ "number=", "string=" ] );
}
Following JSDoc nullable/non-nullable types also supported:
byContract( 42, "?number" ); // a number or null.
byContract( 42, "!number" ); // a number, but never null.
Of course, we can use interfaces for a contract. Thus we can refer any available in the scope objects, including JavaScript built-in interfaces:
var instance = new Date();
byContract( instance, "Date" );
byContract( view, "Backbone.NativeView" );
byContract( e, "Event" );
For arrays and objects, we can optionally validate the content. So we can state that e.g. all of array values must be numbers or all the keys and values of an object are strings:
byContract( [ 1, 1 ], "Array.<number>" );
byContract( { foo: "foo", bar: "bar" }, "Object.<string, string>" );
It may serve well for linear structures, but useless otherwise. So alternatively we can create a type definition describing the content of an object (see byContract.typedef) and refer it as a type afterward.
byContract.typedef( "Hero", {
hasSuperhumanStrength: "boolean",
hasWaterbreathing: "boolean"
});
var superman = {
hasSuperhumanStrength: true,
hasWaterbreathing: false
};
byContract( superman, "Hero" );
This example defines a type `Hero` that represents an object/namespace required to have properties `hasSuperhumanStrength` and `hasWaterbreathing` both of boolean type.
All the described methods validate values by types, but what about invariants? We can wrap the constraint in a custom type. Let's say for testing string is an email address we can add a validator like that:
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 );
}
byContract( "john.snow@got.com", "email" ); // ok
byContract( "bla-bla", "email" ); // Exception!
Actually, you probably don't need event to write a validation function, but use an external library (e.g. validator) instead:
byContract.is.email = validator.isEmail;
Validation logic belongs to the development environment. With byContract we can disable the validation globally with a trigger:
if ( env !== "dev" ) {
byContract.isEnabled = false;
}
byContract is a small validation library (~1KB gzip) that allows you to benefit from Design by Contract programming in your JavaScript code.
Published at DZone with permission of Dmitry Sheiko, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Guide To Selecting the Right GitOps Tool - Argo CD or Flux CD
-
13 Impressive Ways To Improve the Developer’s Experience by Using AI
-
How To Backup and Restore a PostgreSQL Database
-
Batch Request Processing With API Gateway
Comments