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;
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;
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.
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;
},
};
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!';
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;
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.
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;
}),
});
Now, if we try to run this code, it will throw a runtime error:
import { CONSTANT } from './constants';
CONSTANT.SALES_TAX.ALABAMA = 0.08;
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.
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;
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;
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);
If your naming convention for constants looks like this:
// constants.js
export const SALES_TAX_ALABAMA = 0.04;
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;
}),
});
Then you can do this:
import { CONSTANTS } = './constants';
const state = getState();
const salesTaxRate = CONSTANT.SALES_TAX[state.toUpperCase()];
Top comments (12)
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...
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!!!
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.
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.
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.
And in your CONSTANTS module:
Yes. And to further your point, I have in fact created the exact same function in some of my apps before!
Thanks for your reply, Adam!
Keep writing.
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
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
OK. You win...
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 theimport
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 wordsSALES
orTAX
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. 🙂
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.
Great article mate!