This is a collection of alternatives to using if
and other conditional block statements in plain Javascript without module or library dependencies.
Some reasons to consider alternatives:
- boost immutability: because
const
is scope-level, it is unusable in functions usingif
except within the blocks -- for assignment,let
orvar
are the only options, which are mutable - quality of code life:
if
is block statement that has a tendency to beget nesting block statements ad infinitum, and become progressively harder to read as they are added to - avoid refactoring later: many
if
s can usually indicate you really need to break your functions down further, and that your function is doing too much at once - often it should really just be a function: because it is a block-level element, the block can easily grow into something that clearly should be a function, or even many functions
- control side-effects:
switch
statements can easily "leak" when missing abreak
statement, and create "branches" of code without braces, hiding these branches somewhat - testability: each
if
creates a logical branch that cannot be independantly tested - functional: if functional programming is something you want to do more of, you'll generally want less
if
andlet
- be safer with type: when you convert an
if
into a function, you can test for types in TypeScript and Flow
The following examples may or may not adhere to a functional programming style, but can be used as a stepping stone towards more functional code. I tend to prefer functional, but it can be a huge leap to go straight to pure functional.
Each example is a very basic form of a common conditional pattern. These suggestions are meant to give you some ideas of alternatives.
Some examples, such as the "incongruent if
/else
" examples could be considered antipatterns and are themselves likely to be considered by most as poorly-formed if
statements.
let y;
if (x === 1) {
y = 2;
}
Basic, returns y
as false
for default.
// ternary with false default
const y = x === 1 ? 2 : false;
Similar, but has a reusable function that can be memoized.
// functional ternary
const checkX = x => (x === 1 ? 2 : false);
const y = checkX(x);
let y;
if (x === 1) {
y = 2;
} else {
y = 1;
}
Very basic, readable. Allows y
to be a const
.
// ternary assignment
const y = x === 1 ? 2 : 1;
Very similar, but reusable and memoizable.
// functional ternary
const checkX = x => (x === 1 ? 2 : 1);
const y = checkX(x);
let y;
if (x === 1) {
y = 2;
} else if (x === 2) {
y = 17.3;
} else {
y = 1;
}
switch (x) {
case 1:
y = 2;
break;
case 2:
y = 17.3;
break;
default:
y = 1;
}
Use an object to store values as key-value pairs.
// key-value pair list with default
const vals = {
1: 2,
2: 17.3,
default: 1
};
const y = x in vals ? vals[x] : vals.default;
Very similar, and prevents namespace collisions: use a symbol for your default value key. (The example above with default
key would likely misbehave if x
ever had a value called 'default'
.)
// key-value pair list with symbol
// this avoids namespace collision in key names
// note that `$$` is merely a naming style suggestion for Symbols, it is not a requirement at all
const $$defaultSymbol = Symbol();
const vals = {
1: 2,
2: 17.3,
[$$defaultSymbol]: 1
};
const y = x in vals ? vals[x] : vals[$$defaultSymbol];
Or, use an array to store the values. Works only if dealing with low numbers.
// value list accessed by index
const vals = [null, 2, 17.3];
const y = vals[x] || 1;
Use a getter inside a constant function expression as a means of looking up a key-value pair. Allows for memoization, though uses the dreaded this
keyword...
// getter
const vals = x => ({
1: 2,
2: 17.3,
get value() {
return this[x] || 1;
}
});
const y = vals(x).value;
Constant function expression returning a lookup table: zeroth index is the condition, first index is the result.
// conditionals table
const condTable = x => [
[x === 1, 2],
[x === 2, 17.3],
[true, 1]
];
const y = condTable(x)
.find(a => (a[0] ? a[1] : false))
.pop();
let y;
if (x === 1) {
y = 2;
} else if (x === 2) {
y = 17.3;
} else if (z === 4) {
y = 19.2;
{ else if (w === 'something') {
y = 22;
} else {
y = 1;
}
Another constant function expression returning a lookup table, this time taking an array as a parameter. The array param allows for memoization of the table.
// conditionals table with array param
// array-based param allows for memoization if need be
const condTable = ([x, z, w]) => [
[x === 1, 2],
[x === 2, 17.3],
[z === 4, 19.2],
[w === 'something', 22],
[true, 1]
];
const y = condTable([x, z, w])
.find(a => (a[0] ? a[1] : false))
.pop();
What if not just the conditions are mixed, but the results too?
let y;
let j;
if (x === 1) {
y = 2;
} else if (x === 2) {
y = 17.3;
} else if (z === 4) {
y = 19.2;
{ else if (w === 'something') {
y = 22;
{ else if (w === 'something else') {
j = 5;
} else {
y = 1;
}
Similar to conditionals table, use the table to execute functions rather than assign value.
// results are functions, pass everything into params that might be needed
let y, j;
const setY = v => y = v;
const setJ = v => j = v;
const condTable = ([x, z, w]) => [
[x === 1, setY, 2],
[x === 2, setY, 17.3],
[z === 4, setY, 19.2],
[w === 'something', setY, 22],
[w === 'something else', setJ, 5],
[true, setY, 1]
];
condTable([x, z, w])
.find(a => (a[0] ? a[1](a[2]) : false));
let y, z;
if (x === 1) {
y = 2;
if (w > 4) {
z = 1;
}
} else {
y = 1;
if (w < 4) {
z = 2;
}
}
Take each layer and transpose each if
to a function, or assignment.
const fn1 = () => {
y = 2;
z = w > 4 ? 1 : z;
};
const fn2 = () => {
y = 1;
z = w < 4 ? 2 : z;
};
const assignVals = x =>
x === 1
? fn1()
: fn2();