JavaScript Constants With Object.freeze()

Adam Nathaniel Davis - Mar 12 '22 - - Dev Community

This is a dead-simple observation that I wanted to write about because I almost never see this technique used in the wild. Nearly every programmer is familiar with the idea of a "constant". I'm not simply talking about the const keyword in JavaScript. I'm talking about the all-purpose concept of having a variable that is set once - and only once - because, once it's set, that variable's value should never change. In other words, its value should remain constant.

The most common way to do this with modern JavaScript is like so:

const SALES_TAX_ALABAMA = 0.04;
const SALES_TAX_FLORIDA = 0.06;
const SALES_TAX_LOUISIANA = 0.0445;
Enter fullscreen mode Exit fullscreen mode

Once these variable are instantiated with the const keyword, any attempt to reassign them will result in a runtime error. And this approach... "works", but it can be a bit bulky. In every script where you want to leverage these variables, you would need to define them all upfront. And that approach would be unwieldy if these variables are used throughout your codebase.

With modern JavaScript, an alternate approach would be to export these values from a single file. That approach would look like this:

// constants.js
export const SALES_TAX_ALABAMA = 0.04;
export const SALES_TAX_FLORIDA = 0.06;
export const SALES_TAX_LOUISIANA = 0.0445;
Enter fullscreen mode Exit fullscreen mode

This makes our constants universal and far more accessible throughout our app. But I would argue that this is still somewhat bulky. Because every time we want to use any one of these variables inside another file, each variable must be brought into that file with an import.

I also find this approach clunky because it doesn't always play nice with the autocomplete feature in our IDEs. How many times have you been coding when you realize that you need to leverage a constant, like the ones shown above. But you don't remember, off the top of your head, exactly how those variables are named? So you start typing: ALA..., expecting to see the Alabama Sales Tax rate constant pop up.

But your IDE provides no help in autocompleting/importing the value, because there is no constant that begins with "ALA". So after you make a few more misguided attempts to pull up the value by typing the name from memory, you eventually give up and open the constants.js file so that you can read through the whole dang file for yourself to see exactly how those variables are named.


Image description

Objects To The Rescue(???)

This is why I love using JavaScript objects to create namespace conventions. (In fact, I wrote an entire article about it. You can read it here: https://dev.to/bytebodger/why-do-js-devs-hate-namespaces-2eg1)

When you save your values as key/value pairs inside an object, your IDE becomes much more powerful. As soon as you type the initial name of the object, and then type . nearly any modern IDE will helpfully pull up all of the potential keys that exist inside that object.

This means that you can restructure your constants file to look like this:

// constants.js
export const CONSTANT = {
  SALES_TAX: {
    ALABAMA = 0.04;
    FLORIDA = 0.06;
    LOUISIANA = 0.0445;  
  },
};
Enter fullscreen mode Exit fullscreen mode

This supercharges your IDE's autocompletion feature. But... it comes with a downside. Because, in JavaScript, an object that's been defined with the const keyword isn't really a "constant".

With the example above, the following code will throw a much-needed runtime error:

import { CONSTANT } from './constants';

CONSTANT = 'Foo!';
Enter fullscreen mode Exit fullscreen mode

It throws a runtime error because CONSTANT is defined with the const keyword, and we cannot re-assign its value once it's been set. However... this does not necessarily protect the nested contents of the object from being re-assigned.

So the following code will NOT throw a runtime error:

import { CONSTANT } from './constants';

CONSTANT.SALES_TAX.ALABAMA = 0.08;
Enter fullscreen mode Exit fullscreen mode

That's really not very helpful, is it? After all, if any coder, working in any other part of the codebase, can re-assign the value of a "constant" at will, then it's really not a constant at all.


Image description

Object.freeze() To The Rescue(!!!)

This is why I use Object.freeze() on all of my constants. (And it's a simple technique that I've rarely ever seen outside of my own code.)

The revised code looks like this:

// constants.js
export const CONSTANT = Object.freeze({
  SALES_TAX: Object.freeze({
    ALABAMA = 0.04;
    FLORIDA = 0.06;
    LOUISIANA = 0.0445;  
  }),
});
Enter fullscreen mode Exit fullscreen mode

Now, if we try to run this code, it will throw a runtime error:

import { CONSTANT } from './constants';

CONSTANT.SALES_TAX.ALABAMA = 0.08;
Enter fullscreen mode Exit fullscreen mode

Granted, this is somewhat verbose, because you need to use Object.freeze() on every object, even those that are nested inside of another object. In the example above, if you don't freeze the SALES_TAX object, you will still be able to reassign its values.


Image description

A Better Approach

I already know that some devs won't like this approach, because they won't like having to use Object.freeze() on every layer of objects in the constants.js file. And that's fine. There's room here for alternative styles. But I firmly prefer this method for a couple of simple reasons.

A Single Constants File

You needn't use Object.freeze() if you want to maintain a single constants.js file. You can just revert to the "traditional" way of doing things, like this:

// constants.js
export const SALES_TAX_ALABAMA = 0.04;
export const SALES_TAX_FLORIDA = 0.06;
export const SALES_TAX_LOUISIANA = 0.0445;
Enter fullscreen mode Exit fullscreen mode

But I can tell you from decades of experience that it's not too uncommon to open a universal constants.js file that has hundreds of variables defined within it. When this happens, I often find something like this:

// constants.js
export const SALES_TAX_ALABAMA = 0.04;
export const SALES_TAX_FLORIDA = 0.06;
export const SALES_TAX_LOUISIANA = 0.0445;
/*
  ...hundreds upon hundreds of other constants 
  defined in this file...
*/
export const ALABAMA_SALES_TAX = 0.04;
Enter fullscreen mode Exit fullscreen mode

You see what happened there? The file grew to be so large, and the naming conventions were so ad hoc, that at some point a dev was looking for the Alabama sales tax value, didn't find it, and then created a second variable, with an entirely different naming convention, for the same value.

This leads me to my second point:

Objects Promote a Taxonomic Naming Structure

Sure, it's possible for a lazy dev to still define the value for the Alabama sales tax rate twice in the same file. Even when you're using objects to hold those values in a taxonomic convention. But it's much less likely to happen. Because, when you're perusing the existing values in the constants.js file, it's much easier to see that there's already an entire "section" devoted to sales tax rates. This means that future devs are much more likely to find the already-existing value. And if that value doesn't already exist in the file, they're much more likely to add the value in the correct taxonomic order.

This also becomes much more logical when searching through those values with our IDE's autocomplete function. As soon as you type CONSTANTS., your IDE will show you all of the "sub-layers" under the main CONSTANTS object and it will be much easier to see, right away, that it already contains a section dedicated to sales tax rates.

Objects Allow For Variable Key Names

Imagine that you already have code that looks like this:

const state = getState(shoppingCartId);
Enter fullscreen mode Exit fullscreen mode

If your naming convention for constants looks like this:

// constants.js
export const SALES_TAX_ALABAMA = 0.04;
Enter fullscreen mode Exit fullscreen mode

There's then no easy way to dynamically pull up the sales tax rate for state. But if your naming convention for constants looks like this:

// constants.js
export const CONSTANT = Object.freeze({
  SALES_TAX: Object.freeze({
    ALABAMA = 0.04;
    FLORIDA = 0.06;
    LOUISIANA = 0.0445;  
  }),
});
Enter fullscreen mode Exit fullscreen mode

Then you can do this:

import { CONSTANTS } = './constants';

const state = getState();
const salesTaxRate = CONSTANT.SALES_TAX[state.toUpperCase()];
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player