Using JavaScript Case Objects for Input Validation
Building off JavaScript’s remarkable ability to assign protected language values as object keys, we can create input validations using these newfound structures. Starting with our previous function,
function secToMinSec(seconds) {
var minutes = Math.floor(seconds / 60);
var remaining = Math.floor(seconds) - (minutes * 60);
var minutesCases = {
true: minutes + 'm',
false: ''
};
var secondsCases = {
true: remaining + 's',
false: ''
};
var subSecondCases = {
true: '-',
false: ''
};
return [
minutesCases[minutes !== 0],
secondsCases[remaining !== 0],
subSecondCases[seconds < 1]
].join(' ').trim();
};
it’s clear how vulnerable it actually is. Passing it anything other than a number results in a 'NaNm NaNs'
string and, as is obvious from glancing at the function, that outcome isn’t predicted by any part of it. Passing it a negative number will also result in an unanticipated way. For example, secToMinSec(-123)
creates the string,'-3m 57s -'
.
However, this function already provides the template for creating simple, legible validations. By creating case objects that offer the 2 possible scenarios for a given input’s validity, we can look those up and decide what action to take.
var secondsValid = {
true: '',
false: 'Argument must be a Number greater than 0.'
};
The pattern will be
- create an object and follow the naming scheme,
{nameOfInput}Valid
- assign an empty string for
true
- assign an error message for
false
While the empty string may seem a bit strange at first, the reason for the string in false
should be clearer: namely that if the input isn’t valid, we should somehow tell the user that it isn’t and maybe a way to fix it. Perhaps most importantly, we’ve explicitly created outputs for all the scenarios of a validation, one for when it is valid (true
) and one for when it is not (false
).
Just like the return from the function, if we create an array of our validations where the key of each validation case object is a boolean statement, it’ll return the correct look-up’s value:
var errs = [
secondsValid[typeof seconds === 'number')]
].join(' ').trim();
The errs
string that gets generated by this can only be an empty or a non-empty string, so we can create an evaluation for this the same way, with true
and false
as the keys to a case object. However, concatenating strings won’t work beyond that; at this stage, an action must be taken. This can still be captured in a case object since we can simply assign functions to those keys and then invoke them:
var isValid = {
true: proceed,
false: fail
};
isValid[errs.length === 0]();
The fail
function is incredibly simple to define as all it has to do is throw a single error, consisting of the name of the function and the accumulated errors:
function fail() {
throw new Error('secToMinSec: ' + errs);
};
proceed
no less so since we need only move the code from what was serving as the entirety of the secToMinSec
above into the proceed
function.
Putting it all together
function secToMinSec(seconds) {
// Validation //
var secondsValid = {
true: '',
false: 'Argument must be a number greater than 0.'
};
var errs = [
secondsValid[typeof seconds === 'number' && seconds > 0]
].join(' ').trim();
var isValid = {
true: proceed,
false: fail
};
// Invalid //
function fail() {
throw new Error('secToMinSec: ' + errs);
};
// Valid //
function proceed() {
var minutes = Math.floor(seconds / 60);
var remaining = Math.floor(seconds) - (minutes * 60);
var minutesCases = {
true: minutes + 'm',
false: ''
};
var secondsCases = {
true: remaining + 's',
false: ''
};
var subSecondCases = {
true: '-',
false: ''
};
return [
minutesCases[minutes !== 0],
secondsCases[remaining !== 0],
subSecondCases[seconds < 1]
].join(' ').trim();
};
return isValid[errs.length === 0]();
}
Discussion
Thinking and writing about function input validation has come to give me have a very different appreciation for what it means to work in a loosely typed language. Namely, that strongly typed languages provide one sort of validation for you and that it always happens. In JavaScript, if this sort of validation is important to the inner workings of your function, then you need to provide it yourself. Loose typing then both allows you the choice of caring about this validation, just like all your others, and compels you to be explicit about it if you do.
In this example, I used typeof
to make my check, which is not very reliable. For better type-checking, use the is()
function available from JavaScript Garden or the lodash or underscore variations. I use is()
frequently, but I like to flip the arguments so that it reads more naturally to me as a question:
is(seconds, 'Number')
Finally, an important consequence of this set-up is that these validations can not be separated into multiple ones. For example, the following could easily fail in a way that the function can’t account for:
var errs = [
secondsTypeValid[typeof seconds === 'number'],
secondsLengthValid[seconds.length > 0],
].join(' ').trim();
If the input turns out to be undefined
, for example, it will look up false
in the secondsTypeValid
object and start creating its error string, but would throw an unaccounted for JavaScript error for the second one, since undefined
has no length
property. The second validation isn’t inheriting a failed or successful validation from the previous one; it’s simply evaluating whatever’s in the key of the case object and then moving on. As a result, each input must be validated all at once.
There is a certain logic to that, though. A validation is a gatekeeper, deciding whether to let an input into the inner workings of a function. There is no meaningful distinction between passing multiple tests in sequence or all at once before being let in. The end effect is the same: acceptance or rejection.
Benefits
- Has areas that explicitly state what happens when the function is used as intended, when it doesn’t, and how ‘as intended’ gets defined.
- Makes it immediately obvious which scenarios are and aren’t covered.
- Creates consistent, legible input validations.
- Creates a protected interior for the function which may only be accessed by passing validation by way of the
proceed
function. - Ensures that no code that assumes validated inputs ever gets executed.
- Accumulates all the errors at once.
- Use of explicit error messages serve both as a helpful guide when the function is not being used correctly as well as natural language set of requirements.
- Allows the interior function to reference arguments directly, as opposed to having to create sanitized variations, which require more parsing and interpretation on part of an author or maintainer.
- Helps an author consider all the criteria they want to impose on a particular input.