PHPnews.io

Immutable objects in TypeScript

Written by Nehalist.io / Original link on Feb. 12, 2020

immutable-objects-ts.png

Immutability is the concept of not directly mutating objects and instead always work with new objects which reflect the updated object. It makes code easier to debug, performs better, is more predictable and is a requirement for many modern state management solutions.

I was a bit confused about how to handle objects the right way when I first got in contact with the concept of immutability so I'd like to show some of the most basic concepts (adding or deleting properties and changing values) to handle objects in an immutable fashion - and with TypeScript.

Const does not mean immutable

const is often confused with Immutability. Just because you put const in front of a variable does not mean that the variable has become immutable - it just means that you cannot reassign it any longer. An example:

const foo = { value: 42 };
foo.value = 1337; // Completely valid
foo = "bar"; // Not valid!

In an immutable world the re-assignment of foo.value would have failed, because we've mutated the object.

Always keep that in mind when writing or reading the const keyword.

Immutability in TypeScript

As in most cases TypeScript makes life significantly easier and gives us a very simple native type which helps us in achieving immutability: the Readonly<T> type.

const foo: Readonly<{ value: number}> = { value: 42 };
foo.value = 1337; // no longer valid

An important thing to note here is that the Readonly<T> type does not cover nesting:

const foo = Readonly<{ value: { nested: string }}> = {
  value: {
    nested: 'Hello'
  }
};
foo.value.nested = 'World'; // valid!
value.nested is not immutable

If you're interested in creating a DeepReadonly type you can take a look at the issue about DeepReadonly. Other than that you can create some fun things like:

const foo = Readonly<{ value: Readonly<{ nested: string }> }>;

As our value has become immutable by now it may be confusing on how to further work with a real immutable object. How to add properties? How to delete properties? How to change values?

But thanks to things like the Spread syntax and the destructing assignment syntax these things are pretty straight forward.

Adding properties

Adding properties to an immutable object with a certain type is of course not possible as long as the type doesn't change:

type Person = Readonly<{ name: string, age: number }>;

const john: Person = { name: 'John', age: 27 };
john.location = 'Austria'; // invalid, since there's no property "location" on type Person

To add an additional property we need to create an additional type which inherits the initial type. With the spread syntax we can then easily create a new object with the properties from our source object (or even changed values).

type Person = Readonly<{ name: string, age: number }>;
type LocatedPerson = Readonly<Person & { location: string }>;

const john: Person = { name: 'John', age: 27 };
const locatedJohn: LocatedPerson = { ...john, location: 'Austria' };

Changing values

Immutability is all about creating new objects instead of mutating existing ones. Using the spread syntax it's fairly easy to create new objects from existing objects, where the new objects reflects our new values:

type Person = Readonly<{ name: string, age: number }>;

const john: Person = { name: 'John', age: 27 };
const olderJohn: Person = {...john, age: 42 };
Type-hinting olderJohn is not really necessary since TypeScript automatically is able to tell the proper type

olderJohn now shares the exact same properties and values as john, except that the age is 42 instead of 27.

Deleting properties

Sometimes you want to remove properties from an object. In terms of immutability this means we want to create a new object which does not include the property we want to remove. There's a neat little trick using the destructing assignment syntax to make this easy:

type Person = Readonly<{ name: string, age: number }>;
const john: Person = { name: 'John', age: 27 };
const { age, ...agelessJohn } = john;
agelessJohn.age; // invalid, agelessJohn no longer contains an age property

age is the key of the property we want to remove, agelessJohn is our john object - without having an age property any longer.

Libraries

At some point maintaining immutability becomes a bit harder - think of changing the value of a nested property:

// There are ways for DeepReadonly types (https://github.com/microsoft/TypeScript/issues/13923),
// but for the sake of simplicity we'll do it the simple way
type Person = Readonly<{ name: string, location: Readonly<{ address: Readonly<{ zip: number }> }> }>;

const john: Person = {
    name: 'John',
    location: {
        address: {
            zip: 8020
        }
    }
};

john.location.address.zip = 8010; // not valid since zip is readonly

// The correct way
const relocatedJohn: Person = {
    ...john,
    location: {
        ...john.location,
        address: {
            ...john.location.address,
            zip: 8010
        }
    }
}

To compensate for atrocities like that we can simply go for libraries which help for maintaining immutability like Immer or Immutable.js.

But always keep in mind that adding additional dependencies increases maintenance and may not be really necessary, especially since often times the native ways of achieving immutability are already sufficient.

nehalist nehalist nehalist

« Donating BAT to Have I Been Pwned with Brave Browser - Xdebug Update: January 2020 »