Code condition for checking if is time to learn

Enumerators in JavaScript

Should we use it?

Author avatar

David Linhares

Sep 2, 2020 ・ 7 min read

Here we will discuss an introduction to enumerators and how you can use it to better manage the constants file/variables required in your application.

If you ever found a scenario where you have these hardcoded values spread everywhere in your application or having this constants file, large or not, imported everywhere in your code, that holds some specific values (such as URL(s), default strings ...) shared and used across your application and you need to manage it but don't know how to do it in a good way, or just want to learn more about Enumerators in javascript, this article might be useful.

Table of contents

  1. What are enumerators?
  2. When should it be used?
  3. Let's explore the options
  4. Final considerations

What are enumerators?

Enumerators (aka Enums) are a well known concept for keeping constant values in a software application and increase code maintanability by having the 'hardcoded' values written in a single place forcing its usage to be referenced (not re-written).

You might be thinking "I keep all my constant values in a single constants file so I am already using enums, right?" and the answer may not be what you're expecting to hear. Enumerators have two main characteristics:

  1. Store value(s) to be referenced;
  2. The value(s) cannot be changed at runtime.

When should it be used?

Enums are very handy when we need to share some static values between multiple files/modules. Generally speaking whenever we need to use some unchangeable data multiple times we should store that in a single source so we can easily track/change/reference it. Let's say we are working in an online store and we have some fixed prices for shipping given the user region. For the US region the shipping cost is $10 and for the rest of the world it is $20.

In this case we have a couple of options when coding:

  1. Simply check the hardcoded value in the function for the order calculation;
  2. Create a file with the values and import so we can reference to it.

Let's explore the options

Consider the following as the initial implementation for the shipping calculation function:

function calculateShipping(region) {
    if (region === 'US') {
        return 10;
    }
    return 20;
}

In this case it is perfectly fine right, we have the validation for the region value and we are returning the correct amount for the shipping. Like always, in software development things change and now you have some new requirements for this feature. The amount for shipping now covers 4 regions:

  • US $10;
  • CA $12;
  • MX $12;
  • World $20;

It is refactoring time.

function calculateShipping(region) {
    let price;
    switch (region) {
        case 'US':
            price = 10;
            break;
        case 'CA':
            price = 12;
            break;
        case 'MX':
            price = 12;
            break;
        default:
            price = 20;
    }
    return price;
}

Good code right? Now we are aware of all the possibilites for the shipping price and we are not over stating if/elses. Right? No. The requirements changed only for the added region's price but we actually changed the whole logic for the shipping calculation which shows us that the current logic for calculating the shipping price is tight coupled with the prices per region. Multiple things can happen here:

  • We could miss one of the regions that we have a special price for;
  • We could miss the default case for all other regions;

Maybe it is time to keep these special values in a separate file (gotcha)?

// ShippingPrices.js
const ShippingPrices = {
    US: 10,
    CA: 12,
    MX: 12,
    DEFAULT: 20,
};
module.exports = ShippingPrices;

Now let's change the logic for verifying the special shipping prices.

// ShippingCalculator.js
const ShippingPrices = require('./ShippingPrices');

function calculateShipping(region) {
    let price = ShippingPrices.DEFAULT;
    for (const [key, value] of Object.entries(ShippingPrices)) {
        if (region === key) {
            price = value;
        }
    }
    return price;
}

Now we are talking business. A constants file to store all the shipping prices, a for loop in the calculate shipping for validating any custom price, everything is decoupled, we are good now, right? Not yet. We have decoupled the shipping calculation logic from the prices but we only satisfied one Enumerator characteristic (storing static values), what about changing it? What if for some reason (a typo for example) instead of price = value we made value = price? The result could be disastrous. Everytime we match a region we would be updating the value for it to $20 and also always returning $20 which means we are not filtering the prices.

How can we protect the ShippingPrices to be changed? (hint, Enums). Let's create an Enum class for generating the actual Enums:

// Enumerator.js
class Enumerator {
    constructor(object) {
        Object.keys(object).forEach((key) => {
            if ({}.hasOwnProperty.call(object, key)) {
                this[key] = object[key];
            }
        });
        return Object.freeze(this);
    }

    has(key) {
        return {}.hasOwnProperty.call(this, key);
    }
}

module.exports = Enumerator;

Notice how Enumerator.js creates a new object using each property on the object passed in its constructor (to make sure we only compute truthful values) and freezes it preventing to be changed at runtime.

Now we can update the ShippingPrices.js file to export an ShippingPricesEnum:

// ShippingPricesEnum.js
const Enumerator = require('./Enumerator');

const ShippingPrices = {
    US: 10,
    CA: 12,
    MX: 12,
    DEFAULT: 20,
};

module.exports = new Enumerator(ShippingPrices);

And last but not least we can update the calculate shipping function to use it:

// ShippingCalculator.js
const ShippingPricesEnum = require('./ShippingPricesEnum');

function calculateShipping(region) {
    let price = ShippingPricesEnum.DEFAULT;
    for (const [key, value] of Object.entries(ShippingPricesEnum)) {
        if (region === key) {
            price = value;
        }
    }
    return price;
}

The code in ShippingCalculator.js looks almost the same, the main benefit we are getting is not being able to change the enum so even if we assign value = price we would get an error before the code is usable.

We could even improve the calculateShipping for using the has method on the Enumerator:

// ShippingCalculator.js
const ShippingPrices = require('./ShippingPricesEnum');

function calculateShipping(region) {
    let price = 20;
    if (ShippingPrices.has(region)) {
        price = ShippingPrices[region];
    }
    return price;
}

This way we can achieve a safer usage for the enumerator property and also get rid of looping all enumerated properties to a single operation for getting the required value.

Final considerations

Javascript gives the developer a whole lot of flexibility while coding and, like uncle Ben already said, with great power comes great responsibility. Dealing with constant values is a challenge and as time passes and requirements/functionality increases managing hardcoded values can become an issue. Using enumerators can make your codebase more organized and help you manage constant values across the application in such a way that all the enumerated part is decoupled from the logic that uses it. Adding a layer of abstraction in this type of implementation helps on increasing the code maintainability and well define pieces of your code.

If you liked this post don't forget to share it and also let me know in the comments if this article was helpful.

See ya! 👋

Comments

Whipost

Where ideas are shared.

© Whipost 2020