Fabien Huet

Fabien
Huet

Web ninja //
CTO on demand

Home Github About me

🖥️ An alternative to if/else and switch in JavaScript

in JavaScript

I don’t intend to replace them for all their use case. It would be dumb. The alternative won’t change the way it works and its purpose is mostly code aesthetic.

The problem

When you need to set a condition for a value or a function in JavaScript, the first things you try are if/else and switch.

It looks like that:

let foo = '';
switch ( bar ) {
    case 'a':
        foo = 1;
    break;
    case 'b':
        foo = 2;
    break;
    default:
        foo = 3;
    break;
}

let foo = '';
if ( bar === 'a' )
    foo = 1;
else if ( bar === 'b' )
    foo = 2;
else
    foo = 3;

You may not share my opinion. But in my eyes this is verbose and ugly. You write four times foo for no reason: you only want to assign it once. The if/else way evaluatesbar multiple times. And what about these break;?

Note on the ternary operator

When bar can have only 2 values, I always go for the ternary operator. And I write something like that:

let foo = bar === 'a' ? 1 : 2;

But sometime, it’s not enough. For example, if you want to set a default value to handle the case when bar is not “a” or “b”, it becomes ugly :

let foo = ( bar === 'a' || bar === 'b' ) ? ( bar === 'a' ? 1 : 2 ) : 3;

Exceptionally, if you are working with react and you don’t want to define a variable for example, you might do that sometimes. But this is not a good practice. Try to use the ternary operator only when you have two values and when you are sure of these values. Actually, you want to use it most of the time to handle a falsy or nully case against the truthy case.

My 2 coppers

So, what do I do? I’ve seen some articles about switch alternatives calling a function with inside actions; and they work, but I’m not a fan. Why getting data from a function? Just inline the thing! I like the absolute simplicity of getting a value from a basic object literal by it’s key:

let foo = ( {
    a: 1,
    b: 2,
} )[ bar ] || 3;

Yes, it does exactly the same thing that the initial switch did. If bar is a, foo will be 1, if bar is b and if bar is anything else, foo will be 3. But this is so much more elegant. A conditional value with no conditional operator (except for the || to handle the default value)

And yes, this is an expression, so you can use it as a parameter for example. This is valid code:

myFunction( ( {
    a: 1,
    b: 2,
} )[ bar ] || 3 );

I’m a functional-programming lover. The less I write let, var or const, the happier I am.

The very best thing about it is that the values can be defined somewhere else:

let values = {
    a: 1,
    b: 2,
};
let foo = values[ bar ] || 3;

Still the same thing. And you probably use it daily. Just realize that this works as a conational operator.

Of course the values of the properties can be functions returning any value (including a function):

let foo = ( {
    a: () => { return 'a'; },
    b: () => { return myFunction; },
} )[ bar ] || 3;

Yes, the behaviors of this code would be weird, try to get the same “type” for all the properties.

You also can use this pattern to execute code and not only to assign a value. This is valid code:

( ( {
    a: () => {
        console.log( 'function a' );
    },
    b: () => {
        console.log( 'function b' );
    },
} )[ bar ] || ( () => { console.log( 'default function' ); } ) )();

I know that all those parenthesis may seem a bit hard to read. But when you are used to, it’s not. Plus, you have extra parenthesis here to apply the IIFE pattern.

When I write “valid code”, I mean valid esnext code of course, translate that if you want to run it in your console.

Performance

To test the performance, you can run that simple code:

var value = function value() {
    return [ 'a', 'b', 'c' ][ Math.floor( Math.random() * 3 ) ];
};

console.time( 'switch' );
var a1 = [];
for ( var i = 1000000; i >= 0; i-- ) {
    switch ( value() ) {
    case 'a':
        a1.push( 1 );
        break;
    case 'b':
        a1.push( 2 );
        break;
    default:
        a1.push( 4 );
        break;
    }
}
console.timeEnd( 'switch' );

console.time( 'ifElse' );
var a2 = [];
for ( var i = 1000000; i >= 0; i-- ) {
    var bar = value();
    if ( bar === 'a' )
        a2.push( 1 );
    else if ( bar === 'b' )
        a2.push( 2 );
    else
        a2.push( 3 );
}
console.timeEnd( 'ifElse' );

console.time( 'objectLiteral' );
var a3 = [];
for ( var i = 1000000; i >= 0; i-- ) {
    a3.push( ( {
        a: 1,
        b: 2,
    } )[ value() ] || 3 );
}
console.timeEnd( 'objectLiteral' );

I ran it 100 times and got those averages:

switch: 1207.772ms
ifElse: 1541.720ms
objectLiteral: 1297.743ms

The switch will always be the fastest, the if/else always be the slowest and the object literal will be really close form the switch in terms of performances. You decide if it’s worth the performance loss.

This is because of the way the switch is implemented. It evaluates the parameter only once. You can check it by augmenting the length of the values array (and the switch and the if/else…). With a length of 10: return [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' ][ Math.floor( Math.random() * 10 ) ]; it produces those averages :

switch: 1216.430ms
ifElse: 2113.700ms
objectLiteral: 1490.627ms

The performance gap start being huge between the switch and the if else. And it will go up and up with an higher number of possible conditions. The object literal way appears to be slower too; and this is normal since the object has more keys. But it can be greatly improved by simply caching the value like that:

var values = {
    a: 1,
    b: 2,
    c: 3,
    d: 3,
    e: 3,
    f: 3,
    g: 3,
    h: 3,
    i: 3,
    j: 3,
};
for ( var i = 1000000; i >= 0; i-- ) {
    a3.push( values[ value() ] || 3 );
}

Then I got better results (still the average of 100 runs ) :

switch: 1195.124ms
ifElse: 2158.334ms
objectLiteral: 1249.687ms

It went back to “very close to the switch way”.

Conclusion

What do you think? I can totally ear that you find my way uglier than the usual ways. There’s no accounting for taste…