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 (20)
Well yes, but that happens with modularity either way, unless you put all your app in a single file.
I never remember the full name of pretty much anything, so I always end up typing what I remember (like pretty much everyone does). One thing you can do to help out is to have consistent conventions about naming.
This never happened to me, but I think is because I use IDEs/Editors that support fuzzy search. You should try a better editor if you don't get autocomplete for
SALES_TAX_ALABAMAwhile typingALA.You don't actually need objects to organize your constants, you could simply have a
constantsdirectory instead of aconstantsfile, and then namespace them based on filenames instead of objects. So using your same example:You then go over the fact that you can still mutate objects, but I think this is more like an XY problem: You're going deep into patching the issues of a solution that is not ideal in the first place.
You can also use JSDocs or TypeScript to inform devs when they are trying to do mutations where they shouldn't, and this doesn't have any effect in runtime (which is what you want):
If you really want to have everything in a single file, you still can with the approach I suggested of having a
constantsdirectory. You just add anindex.jsfile to it and then:And you do that with all your file based namespaces.
Not really. As I pointed out, you can solve this with a few more files, and you make them more discoverable, easier to find and organize.
Because imports can be used as objects, you don't need to define them as objects to do that. You can just:
Closing thoughts: I see two big issues with the post:
Hope this helps with your struggles with auto-completion and constants!
Cheers!
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!!!
Why is that? If you have objects leaking into a global/shared place that could maliciously or accidentally altered in runtime, that's again an entire different problem (feels like a XY again). Don't get me wrong, maybe there is some real world scenario I'm not considering in which freezing an object could be useful, but I never had to deal with that.
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.
Oh yup, we are in different boats then. I prefer to know that my code will fail before it does, and create an architecture that doesn't enable unwanted mutations at runtime, instead of throwing when the mutation happens. What if that frozen object you're putting on an exposed place is mutated very rarely? You'll realize that you had a mistake only if you're "lucky enough" to run that 😅
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.
Yup, but my point is not only based on TS or JSDocs, but also on avoiding an architecture that exposes mutable objects. The TL;DR would be: Object.freeze solves an issue you shouldn't have in the first place.
PS: I also have an article about a similar TS argument, and I don't agree with it either, mainly because types and validations are two different things, and TS only does types.
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.
You can have an architecture that doesn't expose mutable objects without TS or JSDocs. Is an architecture issue, not a type issue 🙂
OK. You win...
😕 is not about "winning". I'm sharing another perspective to your post so folks have other perspectives to evaluate. Disagreeing is not necessarily an argument to be won.
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 theimportstatement.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 wordsSALESorTAXare 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. 🙂
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
Great article mate!