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 | function add(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 | function add(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 | function add(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 | function isPerson(p) { |
Instead of this multi-path if/else code we can validate against a schema object using check.all method
1 | var 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 | var apiUrlFormat = /.../; |
The negation and return are completely unnecessary and can be replaced with shorter code
1 | function isApiUrl(url) { |
Conditional validation
If an input argument is optional, we usually write blocks like this one
1 | function isPerson(p) { |
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 | function isPerson(p) { |
We can refactor this by moving optional check into the schema object
1 | var 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 | !check.number(foo) |
Using not
modifier helps to keep the condition with multiple parts look and read the same
1 | // hard to read condition |
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 | var personSchema = { |
Update 2
I have added check.defend(fn, predicates) method to separate function's logic from defensive checks. Instead of writing
1 | function add(a, b) { |
You can write just the addition login and then add input argument checks dynamically
1 | var add = (function () { |