JavaScript is not so strong when it comes to data immutability. Internally, all primitive data types like string
, number
, boolean
etc. are immutable which means you can’t mutate the value once it is set to a variable. You can only assign a new value to a variable and the old one will be garbage collected by the JavaScript engine when the time comes.
That’s not the same story with objects. If you create a plain object using an object literal, you can override the value of a property or add or remove a property to or from an object whenever you want.
var obj = { a: 1 }; // define an object
obj.b = 2; // add new property
obj.c = "hello"; // add new property
obj.b = 3; // update property value
delete obj.b; // delete property
Objects are by default mutable as shown above and they are passed by reference (but not in a way that operates in other languages). When you assign an object to a variable, the object value doesn’t get copied. JavaScript only assigns a reference of the object to the new variable. The same principle applies when you pass an object as an argument to a function or returns an object from a function. However, there are preventive mechanisms you can apply to make objects immutable.
JavaScript provides Object.defineProperty
function to add a new property on an object with customized property settings. You can also use this function to modify settings of an existing property on an object. This setting of an individual property is called a property descriptor.
💡 You can follow the MDN documentation to know more about the property descriptor or read the first part of my article on JavaScript decorators.
In the above example, we have created a ross
object using object literal expression. Since by default, object properties are writable, we can assign a new value to firstName
property and we can see it changing.
The writable
option of the property descriptor if set to false
makes the property read-only. Hence even though we have assigned string value Jack
to the ross.firstName
, the value stays the same. However, in strict mode, it will throw an error since we are trying to modify the value of a read-only property.
If you want to add new properties or modify existing properties with custom property descriptors at once, then you can use Object.defineProperties
function. If you want to create a fresh object with custom property descriptors, then you can go for Object.create
function.
Whichever the case, handling data immutability is tough in JavaScript. But there are some quick and easy methods that you can implement to make your life less miserable.
JavaScript provides Object.[freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)
method which basically freezes an object like ice. You won’t be able to add or remove properties to or from the object. You also won’t be able to override the values of the properties or configure their property descriptors. Let’s see a simple example.
In the above example, we have created a simple ross
object. This object is mutable in every which way, so we froze it using Object.freeze
method. Now the object is basically dead. As you can see, we tried to override a property value and add a new property, but nothing happened. In strict mode, this operation will throw an error at runtime.
💡 The reason we used
ross as any
type assertion because TypeScript won’t let you access or assign a property that doesn’t exist on the objectross
object which contains only thefirstName
andlastName
properties.
JavaScript also provides Object.seal
method which is less aggressive compared to freeze
method. This method prevents a new property being added to the object or configuring existing properties. It won’t prevent you from assigning new values to properties as long as they are writable.
💡 The most important thing to be careful about
Object.freeze
orObject.seal
methods are that these methods only work on the properties of an object. If the value of a property is another object, its properties won’t be affected. So techically, your object is not immutable as one can modify the nested property value.
Immutability using the const keyword
ES6 brought a lot of good features to the language one of which is the let
and const
keywords. We were used to declaring variables using var
but there was no mechanism to create constants in JavaScript. But in modern JavaScript, const
declares a constant.
When a variable is declared using const
, its value is fixed as in you won’t be able to mutate it, ever. The const
keyword, however, is the most misunderstood one. A constant doesn’t make the value immutable, it just prevents the assignment of the new value to the variable name.
In the above program, we created a constant ross
which is a plain JavaScript object. We can still modify the firstName
and lastName
property values since they are not read-only. However, we won’t be able to change the value of ross
constant using a value assignment syntax.
Immutability using the readonly keyword
The const
keyword is a JavaScript keyword which means you can make a variable immutable natively. However, TypeScript provides a readonly
keyword that can be used as a compile-time check to avoid mutation of object properties, class properties, array, etc.
Read-only Interface fields
TypeScript provides readonly
keyword that can be used to annotate a field in an interface as read-only. Any attempt to override the property value marked readonly
will result in a compile-time error.
In the above program, the firstName
field of the Person
interface is read-only. Since ross
is a type of Person
, TypeScript won’t let us assign a new value to the firstName
field.
💡 TypeScript doesn’t modify the property descriptor of the
firstName
property in the compiled JavaScript code. Thereadonly
keyword is just a compile-time check but it should suffice the need in most of the cases.
Read-only Class fields
Like an interface, we can mark fields of a class read-only. We need to use the same readonly
keyword before the field name declarations.
In the above example, we have marked firstName
field of the Person
class read-only. We are allowed to set the initial value of the firstName
field from within the constructor function. But once the instance object is created, we won’t be able to mutate its value.
💡 You can also use
public
,private
orprotected
access modifiers withreadonly
keyword provided that access modifier keyword should come before thereadonly
keyword. You can also use parameter property declaration using justreadonly
keyword or alongside an access modifier.
Array Immutability
There is no efficient way to freeze arrays in JavaScript at the moment. Hence all you can do is hope that someone in your team hasn’t written a code that mutates an array. But TypeScript does have a mechanism to prevent this.
You can use readonly
keyword in the type annotation of an array. This instructs TypeScript to forbids any value addition to the array or changing the length of an array. Though this doesn’t prevent assigning a new array value to the array variable, hence there is no harm throwing const
keyword in there.
As you can see from the above program, any attempt to modifying the internal structure of an array resulted in failure. Similar to an object, arrays are also passed by reference since an array is basically an object. Hence, the TypeScript compiler won’t let you assign a read-only array to a mutable array.
💡 You can also use
ReadonlyArray
global utility type provided by the TypeScript to create read-only arrays but we will look into this in the next tutorial. Similar to array, you can also create read-only tuple by puttingreadonly
keyword before a tuple type such asreadonly [number, string]
.