how to make regular JavaScript objects behave like primitive values. Now, let’s look closely at the usefulness of primitive objects, exploring how reducing capabilities could be a benefit for your project.
Writing programs in JavaScript is approachable at the beginning. The language is forgiving, and you get accustomed to its affordances. With time and experience working on complex projects, you start to appreciate things like control and precision in the development flow.
Another thing you might start to appreciate is predictability, but that’s way less of a guarantee in JavaScript. While primitive values are predictive enough, objects aren’t. When you get an object as an input, you need to check for everything:
- Is it an object?
- Does it have that property you’re looking for?
- When a property holds
undefined
, is that its value, or is the property itself missing?
It’s understandable if this level of uncertainty leaves you slightly paranoid in the sense that you start to question all of your choices. Subsequently, your code becomes defensive. You think more about whether you’ve handled all the faulty cases or not (chances are you have not). And in the end, your program is mostly a collection of checks rather than bringing real value to the project.
By making objects primitive,
many of the potential failure points are moved to a single place — the
one where objects are initialized. If you can make sure that your
objects are initialized with a certain set of properties and those
properties hold certain values, you don’t have to check for things like
the existence of properties anywhere else in your program. You could
guarantee that undefined
is a value if you need to.
Let’s look at one of the ways we can make primitive objects. It’s not the only way or even the most interesting one. Rather, its purpose is to demonstrate that working with read-only objects doesn’t have to be cumbersome or difficult.
Note: I also recommend you to check the first part of the series, where I covered some aspects of JavaScript that help bring objects closer to primitive values, which in return allows us to benefit from common language features that aren’t usually associated with an object, like comparisons and arithmetic operators.
Making Primitive Objects In Bulk
The most simple, most primitive (pun intended) way to create a primitive object is the following:
This single line results in an object that can represent anything. For instance, you could implement a tabbed interface using an empty object for each tab.
If you’re like me, that tabs
element just screams to be reworked. Looking closely, you’ll notice
that tab elements are similar and need two things, such as an object reference and a label string. Let’s include the label
property in the tabs
objects and move the objects themselves into an array. And since we’re not planning to change tabs
in any way, let’s also make that array read-only while we’re at it.
That
does what we need, but it is verbose. The approach we’ll look at now is
often used to hide repeating operations to reduce the code to just the
data. That way, it is more apparent when the data is incorrect. What we
also want is to freeze
objects (including the array) by
default rather than it being something we have to remember to type out.
For the same reason, the fact that we have to specify a property name
every time leaves room for errors, like typos.
To easily and consistently initialize arrays of primitive objects, I use a populate
function. I don’t actually have a single function that does the job. I
usually create one every time based on what I need at the moment. In the
particular case of this article, this is one of the simpler ones.
Here’s how we’ll do it:
If that one feels dense, here’s one that’s more readable:
With that kind of function at hand, we can create the same array of tabbed objects like so:
Each array in the second call represents the values of resulting objects. Now let’s say we want to add more properties. We’d need to add a new name to the first call and a value to each array in the second call.
Given some whitespace, you could make it look like a table. That way, it’s much easier to spot an error in huge definitions.
You may have noticed that populate
returns another function. There are a couple of reasons to keep it in
two function calls. First, I like how two contiguous calls create an
empty line that separates keys and values. Secondly, I like to be able
to create these sorts of generators for similar objects. For example,
say we need to create those label objects for different components and
want to store them in different arrays.
Let’s get back to the example and see what we gained with the populate
function:
Using primitive objects makes writing UI logic straightforward.
“
Using functions like populate
is less cumbersome for creating these objects and seeing what the data looks like.
Check That Radio
One of the alternatives to the approach above that I’ve encountered is to retain the active
state — whether the tab is selected or not — stored as a property of the tabs
object:
This way, we replace tab === active
with tab.selected
. That might seem like an improvement, but look at how we would have to change the selected tab:
Because this is logic for a radio button, only a single element can be selected at a time. So, before setting an element to be selected, we first need to make sure that all the other elements are unselected. Yes, it’s silly to do it like that for an array with only two elements, but the real world is full of longer lists than this example.
With a primitive object, we need a single variable that represents the selected state. I suggest setting the variable on one of the elements to make it the currently-selected element or setting it to undefined
if your implementation allows for no selection.
With multi-choice elements like checkboxes, the approach is almost the same. We replace the selection variable with an array. Each time an element is selected, we push it to that array, or in the case of Redux, we create a new array with that element present. To unselect it, we either splice it or filter out the element.
Again, this is straightforward and concise. You don’t need to remember if the property is called selected
or active
;
you use the object itself to determine that. When your program becomes
more complex, those lines would be the least likely to be refactored.
In the end, it is not a list element’s job to decide whether it is selected or not. It shouldn’t hold this information in its state. For example, what if it’s simultaneously selected and not selected in several lists at a time?
Alternative To Strings
The last thing I’d like to touch on is an example of string usage I often encounter.
Text is a good trade-off for interoperability. You define something as a string and instantly get a representation of a context. It’s like getting an instant energy rush from eating sugar. As with sugar, the best case is that you get nothing in the long term. That said, it is unfulfilling, and you inevitably get hungry again.
The problem with strings is that they are for humans. It’s natural for us to distinguish things by giving them a name. But a program doesn’t understand the meaning of those names.
“
Your program only knows whether two strings are equal or not. And even then, telling whether strings are equal or unequal doesn’t necessarily provide an insight into whether or not any of those strings contain a typo.
Objects provide more ways to see that something is wrong before you run your program. Because you cannot have literals for primitive objects, you would have to get a reference from somewhere. For example, if it’s a variable and you make a typo, you get a reference error. There are tools that could catch that sort of thing before the file is saved.
If
you were to get your objects from an array or another object, then
JavaScript won’t give you an error when the property or an index does
not exist. What you get is undefined
, and that’s something
you could check for. You have a single thing to check. With strings, you
have surprises you might want to avoid, like when they’re empty.
Another
use of strings I try to avoid is checking if we get the object we want.
Usually, it’s done by storing a string in a property named id
.
Like, let’s say we have a variable. In order to check if it holds the
object we want, we might need to check if a string in the id
property matches the one we expect it to. To do that, we would first
check if the variable holds an object. If the variable does hold an
object, but the object lacks the id
property, then we get undefined
,
and we’re fine. However, if we have one of the bottom values in that
variable, then we are unable to ask for the property directly. Instead,
we have to do something to either make sure that only objects arrive at
this point or to do both checks in place.
Here’s how we can do the same with primitive objects:
The benefit of strings is that they are a single thing that could be used for internal identification and are immediately recognizable in logs. They sure are easy to use right out of the box, but they are not your friend as the complexity of a project increases.
I find there’s little benefit in relying on strings for anything other than output to the user. The lack of interoperability of strings in primitive objects could be solved gradually and without the need to change how you handle basic operations, like comparisons.
Wrapping Up
Working directly with objects frees us from the pitfalls that come with other methods. Our code becomes simpler because we write what your program needs to do. By organizing your code with primitive objects, we are less affected by the dynamic nature of JavaScript and some of its baggage. Primitive objects give us more guarantees and a greater degree of predictability.
No comments:
Post a Comment