Readable conditions using check-types.js

Clear and easy to read defensive programming with check-types.js and check-more-types.js

I practice defensive programming where each function starts by checking its input arguments. If any input has unexpected value, the function throws an exception or returns false. The number and strictness of checks is directly proportional to the defensive distance. I described a number of predicates one can use to check input arguments in Defensive coding examples.

In this post I will describe how to transform clunky input checks into compact and clear predicate functions. As base I will use a library of predicates check-types.js. We love this library and extended it with additional predicates and published the extension as check-more-types.

Predicate vs assertion

A typical predicate function returns true / false and can be used like this

1
2
3
4
5
6
function add(a, b) {
if (!check.number(a) || !check.number(b)) {
return NaN;
}
return a + b;
}

check-types.js has each predicate function added to check.verify object that throws an exception if predicate returns false. I prefer throwing an exception to signal unexpected input

1
2
3
4
5
function add(a, b) {
check.verify.number(a, 'invalid first argument ' + a);
check.verify.number(b, 'invalid second argument ' + b);
return a + b;
}

Notice that we concatenated error message in order to provide meaningful details. The string concatenation is an expensive operation, and in most cases the result is wasted (the predicate returns true). This is why I prefer using lazy assertions to throw exceptions. This library only concatenates arguments if predicate returns false, avoiding performance penalty hit

1
2
3
4
function add(a, b) {
lazyAss(check.number(a) && check.number(b), 'invalid add arguments', a, b);
return a + b;
}

I will leave assertions out of the scope, instead describing common predicate patterns. In each case I will show the predicate code before and after refactoring. In each instance, I hope to get simpler and more readable function.

Checking against schema

Validating an object with several properties against a schema is the first common pattern. Imagine we have an object describing a person. We require three properties: first and last names, and address. We will require name parts to be valid strings (non-empty), but we do not know how the address is going to be specified. Here is our initial code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function isPerson(p) {
if (!p) {
return false;
}
if (!check.unemptyString(p.first)) {
return false;
}
if (!check.unemptyString(p.last)) {
return false;
}
if (!check.has(p, 'address')) {
return false;
}
return true;
}
isPerson({
first: 'john',
last: 'smith'
});
// returns false (p does not have address property)

Instead of this multi-path if/else code we can validate against a schema object using check.all method

1
2
3
4
5
6
7
8
9
var personSchema = {
first: check.unemptyString,
last: check.unemptyString,
address: check.defined
};
function isPerson(p) {
return check.object(p) &&
check.all(p, personSchema);
}

Removing negation

I noticed that my first coding pass always writes code that tries to return early on obviously bad input. Here is an example that validates url string against regular expression, but only if it a string

1
2
3
4
5
6
7
var apiUrlFormat = /.../;
function isApiUrl(url) {
if (!check.unemptyString(url)) {
return false;
}
return apiUrlFormat.test(url);
}

The negation and return are completely unnecessary and can be replaced with shorter code

1
2
3
4
function isApiUrl(url) {
return check.unemptyString(url) &&
apiUrlFormat.test(url);
}

Conditional validation

If an input argument is optional, we usually write blocks like this one

1
2
3
4
5
6
7
8
9
10
function isPerson(p) {
if (!check.object(p)) {
return false;
}
// address is an optional string
if ('address' in p && check.unemptyString(p.address)) {
return false;
}
return check.all(p, personSchema);
}

Optional values that are either null or undefined can be checked using predicates in check.maybe object (see modifiers). Refactored code is a one-liner

1
2
3
4
5
function isPerson(p) {
return check.object(p) &&
check.all(p, personSchema) &&
check.maybe.unemptyString(p.address);
}

We can refactor this by moving optional check into the schema object

1
2
3
4
5
6
7
8
9
var personSchema = {
first: check.unemptyString,
last: check.unemptyString,
address: check.maybe.unemptyString
};
function isPerson(p) {
return check.object(p) &&
check.all(p, personSchema);
}

Inverse predicate

check-types.js has automatic negation for each predicate function in check.not object. For example these two statements are equivalent

1
2
!check.number(foo)
check.not.number(foo)

Using not modifier helps to keep the condition with multiple parts look and read the same

1
2
3
4
5
// hard to read condition
function isValidId(id) {
return check.string(id) &&
!check.emptyString(id);
}

We can transform into positive predicates

function isValidId(id) {
  return check.string(id) &&
    check.not.emptyString(id);
}

Adding your own predicates

check-more-types exposes a method to add your own functions as predicates to the check object including its modifiers. For example, let us add the above isValidId function

check.mixin(isValidId, 'id');
// check.id, check.not.id, check.maybe.id are created
// a person can have id assigned
var personSchema = {
  first: check.unemptyString,
  last: check.unemptyString,
  address: check.maybe.unemptyString,
  id: check.maybe.id
};
function isPerson(p) {
  return check.object(p) &&
    check.all(p, personSchema);
}

The second parameter name in check.mixin(fn, name) is optional. If missing, the code will try to use fn.name property, which is risky, since functions can be renamed during minification step. Thus I advise to always set predicate's name explicitly.

Callback safety

All predicates in check-types.js and check-more-types are safe to use as callbacks.

function arePeople(people) {
  return check.array(people) &&
    people.every(check.isPerson);
}

You can even combine predicates on the fly using helper and function

function and() {
  var predicates = Array.prototype.slice.call(arguments);
  return function evaluateAnd() {
    var args = Array.prototype.slice.call(arguments);
    return predicates.every(function (fn) {
      return fn.apply(null, args);
    });
  };
}
var peopleExist = and(check.unemptyArray, arePeople);
peopleExist([...]);

Conclusion

After refactoring checks using the above patterns, I advise to refactor the conditions even further by eliminating && and || boolean functions. See my posts Refactoring OR and Refactoring AND. Read "Breaking Down Giant Expressions" in excellent The Art of Readable Code.

Update 1

I have added check.schema(schema, object) method to the check-more-types library that is inverse of check.all. Placing schema object first allows creating validation method simply by using Function.prototype.bind, for example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var personSchema = {
name: check.unemptyString,
age: check.positiveNumber
};
var isValidPerson = check.schema.bind(null, personSchema);
var h1 = {
name: 'joe',
age: 10
};
var h2 = {
name: 'ann'
// missing age property
}
isValidPerson(h1); // true
isValidPerson(h2); // false

Update 2

I have added check.defend(fn, predicates) method to separate function's logic from defensive checks. Instead of writing

1
2
3
4
5
function add(a, b) {
la(check.number(a), 'first argument should be a number', a);
la(check.number(a), 'second argument should be a number', b);
return a + b;
}

You can write just the addition login and then add input argument checks dynamically

1
2
3
4
5
6
7
8
9
10
11
var add = (function () {
// inner private function without any argument checks
function add(a, b) {
return a + b;
}
// return defended function
return check.defend(add, check.number, check.number);
}());
add(2, 3); // 5
add('foo', 'bar');
// [Error: Argument 1 foo does not pass predicate]