DEV Community

Cover image for JavaScript Constants With Object.freeze()
Adam Nathaniel Davis
Adam Nathaniel Davis

Posted on • Updated on

JavaScript Constants With Object.freeze()

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

Top comments (12)

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Code organization: With a little better organization, you don't need to use objects and freeze everything. You can just use the file system and better conventions.

I've not seen, in my IDE, an ability to perform autocompletions based on file structure. But I'll have to play around with that some...

Object freezing: Object freezing is generally there to avoid unwanted mutations, but with JSDocs or TypeScript types, you can avoid those mutations quite easily.

Well, yes and no. My main concern is that I want unwanted mutations to be stopped at runtime. This is one of my big qualms with TS.

I truly appreciate your thoughtful replies!!!

 
bytebodger profile image
Adam Nathaniel Davis

I wrote a whole article about this previously (dev.to/bytebodger/tossing-typescri...). I find TS's type "safety" to be illusory in a frontend/web-based environment. Not asking you to agree with me. If I'm writing Node apps, then I find TS's typing to be much more useful. But when I'm building frontend apps, I feel strongly that it's a false security blanket. Again, not asking you to agree with me on that one. It's just a strong conviction based upon my past experiences.

 
bytebodger profile image
Adam Nathaniel Davis

Because I want to know that, any time an attempt is made to mutate the object, the code will fail. I'm rarely interested in compile time. I'm interested in whether or not my code operates properly at runtime.

Collapse
 
synthetic_rain profile image
Joshua Newell Diehl

One can use a recursive function similar to this one to freeze an object along with any objects nested within.
Obviously there are performance concerns depending on object size.

function deepFreeze(nestedObject) {
  const propNames = Object.getOwnPropertyNames(nestedObject);
  // Traverse
  for (const name of propNames) {
    const value = nestedObject[name];
    // Check for reference-type value
    if (value && typeof value === "object") {
      // Recurse
      deepFreeze(value);
    }
  }
  // Freeze each of type "object"
  return Object.freeze(nestedObject);
}
Enter fullscreen mode Exit fullscreen mode

And in your CONSTANTS module:

export const CONSTANTS = deepFreeze({
  SALES_TAX: {
    ALABAMA = 0.04;
    FLORIDA = 0.06;
    LOUISIANA = 0.0445;
  },
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Yes. And to further your point, I have in fact created the exact same function in some of my apps before!

Collapse
 
synthetic_rain profile image
Joshua Newell Diehl

Thanks for your reply, Adam!
Keep writing.

Collapse
 
clamstew profile image
Clay Stewart

Maybe use the deepfreeze npm package instead of nested Object.freeze. Solid technique. I’ve used it myself in prod for years. Have to agree with the file vs nested convention though. Nesting will get gross

Collapse
 
mcsee profile image
Maxi Contieri • Edited

Great article!

consts are great but introduce coupling.
For example you cannot replace it them in tests
That's why, IMHO, we should use objects and functions with Dependency injection.
In your brilliant article, Frozen Objects and you can "replace them" in tests

 
bytebodger profile image
Adam Nathaniel Davis

OK. You win...

 
mindplay profile image
Rasmus Schultz

Group things into modules - that's what modules are for. Don't create arbitrary structures to work around an imagined problem or lacking IDE support.

Import modules with import * if you have to iterate over the keys - if you need a specific tax rate only, import that; don't import symbols you don't use. (So a person can make sense of dependencies, and so tree-shaking works better.)

I wonder what IDE you're using? In either WebStorm or VS Code, if I type SALES_ and hit CTRL+SPACE, it will even automatically add the import statement.

So I honestly don't even know the issue you're trying to work around.

The key point for me here is, don't add arbitrary structure to accommodate an IDE. Create the data structures you need. Use modules to group related symbols, the way they were intended. Any modern IDE should be more than able to keep up with that.

In fact, simpler patterns are usually better for IDE support. I mean, someone now has to know to find and import a symbol with an arbitrary name like CONSTANTS, which does not relate to anything in your business domain - whereas a name including the words SALES or TAX are immediately and easily found by an IDE, and easily recognized and confirmed by the user when they see a module name that matches in auto-complete.

In my opinion, you were doing it right to start with. 🙂

 
bytebodger profile image
Adam Nathaniel Davis

It solves a problem you shouldn't have - if you're already using TS or JSDocs. For reasons that I've explained in lonnnnng detail in previous posts, I'm not using TS. I don't have any "problem" with JSDocs (in fact, I've recommended it here dev.to/bytebodger/a-jsdoc-in-types...), but I don't often use that either.

Collapse
 
jackbrownandhiskeyboard profile image
Jack Brown

Great article mate!