Achieving Immutability in JavaScript and TypeScript
Published on
6 min read
In JavaScript and TypeScript, immutability is
essential for creating stable, predictable code. Immutable objects don’t change after creation, which simplifies debugging, enhances
performance in some contexts, and helps avoid side effects. In this post, we’ll explore several methods to achieve immutability, including
Object.assign()
, the spread operator (...
), and structuredClone()
. We’ll dive into how these methods work, highlighting
shallow vs. deep cloning and providing code snippets for clarity.
Why Immutability?
Immutability prevents unintended modifications to an object by ensuring that any changes result in a new object. This approach is particularly valuable in applications where state management is crucial, like in React. By creating new copies instead of modifying existing objects, we avoid confusing reference-based bugs and improve application predictability.
Shallow Copy with Object.assign()
Object.assign()
is often used for copying objects, but it only performs a shallow copy. That means it only copies the properties at
the top level. Nested objects or arrays still reference the same locations in memory, leading to potential unintended changes.
As shown, modifying shallowCopy.b.c
affects original.b.c
since both point to the same nested object. For a fully immutable structure,
you need a deep copy, which Object.assign()
can’t provide by itself.
Shallow Copy with the Spread Operator (...
)
The spread operator (...
) is a more modern syntax for creating shallow copies. It offers a concise alternative to Object.assign()
for the same purpose:
While the spread operator is convenient, it, too, performs a shallow copy. To avoid unintended mutations, any nested objects need to be individually spread to ensure complete immutability.
Spreading Nested Objects
To deeply clone an object, we need to spread each level manually. This approach becomes cumbersome for deeply nested objects, but it works for relatively simple structures.
Here, b
and b.d
are each spread separately, ensuring a true deep copy. If your object structure is more complex,
however, manually spreading at each level may not be practical.
Spreading Multiple Objects
Another nice thing about the spread operator is that you can spread multiple objects to create a new object, where the properties of the last object will overwrite those of the previous onces if they have the same keys. For example:
In this example, the property b
from obj2
overwrites the property b
from obj1
because obj2
is spread after obj1
.
Handling Undefined Properties
Another important aspect to consider is how undefined properties are handled. If you spread an object with a property that has a defined
value, and then spread another object where that property is undefined
, the property will not be removed. Instead, it will retain its
original value unless explicitly set to undefined
. For example:
In this example, we spread the properties of obj1
and obj2
into newObj
. Normally, spreading obj2
would not update the property b
to undefined
because the spread operator does not remove properties; it only adds or overwrites them. However, by explicitly setting b
to obj2.b
after spreading, we ensure that b
in the resulting object is set to the value of obj2.b
, which is undefined
in this case.
This way, b
is correctly updated to undefined
in newObj
.
Deep Copy with structuredClone()
For true deep cloning, the built-in structuredClone()
function is a great solution in modern JavaScript environments. It creates a
complete, recursive copy of the object, copying all levels of nested objects and arrays without retaining references.
Limitations of structuredClone()
While structuredClone()
is very convenient, it has some limitations. For instance, it doesn’t clone functions, DOM elements, or class
instances. It’s also less widely supported in older environments, so you may need a polyfill or alternative for backward compatibility.
Compatibility of structuredClone()
According to the Node.js documentation, the structuredClone
function was introduced in version v17.0.0
. Currently, at the time of writing this, the oldest Long-Term Support (LTS) version is v18.24.0
,
meaning that most modern Node.js environments support structuredClone
.
For browsers, according to Can I use, structuredClone
is supported by approximately 94.21%
of users worldwide. This makes it a viable choice if your application targets recent versions of major browsers. If you only need to support
the latest couple of releases, you should be able to use structuredClone
confidently.
For older browsers, you can include a polyfill that falls back to the JSON
method for deep cloning, which is discussed below.
JSON Methods for Simple Deep Cloning
In cases where the object structure is simple, a common hack for deep cloning is to convert the object to a JSON string and then parse it back into an object. This approach removes all references to nested objects, effectively creating a deep copy. However, it doesn’t work with functions or other complex data types like dates.
This method is useful for simple objects but has drawbacks: it removes all functions, dates, and other non-JSON-compatible data.
Choosing the Right Method for Immutability
The best method for immutability depends on your needs:
- Shallow Copy: Use
Object.assign()
or the spread operator when you only need a shallow copy of simple objects. - Deep Copy: Use
structuredClone()
for complex structures orJSON.parse(JSON.stringify())
for JSON-compatible objects without functions or dates. - Manual Nested Spread: For situations where
structuredClone()
isn’t available, manually spreading nested objects can provide immutability but requires more maintenance.
Conclusion
Immutability is crucial for reliable, predictable JavaScript and TypeScript applications. By understanding the nuances of shallow and deep copying, you can choose the right approach for each scenario. Whether you’re managing state in a React app or avoiding side effects in a complex program, these techniques give you the tools to create safer, more robust code.
Share on social media platforms.