Krish <Dev />
ArticlesTutorialsCourses

Immutable Object and Array using Typescript

Hi folks, in this article, we will use Typescript to create immutable object and array. Let's see how simple it is.

immutable-object

Primitive Types

We all know that in Typescript we can use const variable declaration to prevent reassigning the value to the variable; this will work with all of the primitive types. (boolean | number | string | symbol | null | undefined)

const iAmAwesome = true;
iAmAwesome = false; // Cannot assign to 'iAmAwesome' because it is a constant

We can say that iAmAwesome variable is immutable as its value will always be true until the program is terminated.

Object

But how about this case, the variable that has the object as the value?

interface User {
  id: number;
  name: string;
}

const user: User = {
  id: 1,
  name: "Keerati"
};

user.id++;
user.name = "George";

There is nothing wrong with this; we can change the id and name of the user variable even it's declared by const.

In the real system, the user will be retrieved from the database and changing the id of it is not what we are expecting. How can we stop others from doing this?

Typescript introduces readonly keyword which we can use for this case.

interface User {
  readonly id: number;
  name: string;
}

const user: User = {
  id: 1,
  name: "Keerati"
};

user.id++; // cannot assign to 'id' because it is a read-only property.

After using readonly keyword on id field, you will see the error from Typescript service when trying to change its value.

Yup, readonly will prevent properties of the object from being reassigned.

Array

const numbers: number[] = [1, 2, 3, 4, 5];
numbers.push(6); // There is nothing wrong with this

In this case, we assign an array of numbers to numbers variable and use the push method to add an element to it. We all know that we can do this.

How to stop this array from mutation?

const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
numbers.push(6); // Property 'push' does not exist on type 'ReadonlyArray<number>'

Changing the type from number[] to ReadonlyArray<number> will remove all of the mutable methods (e.g., push, splice, sort, reverse) from numbers variable.

Put it all together

interface User {
  readonly id: number;
  readonly name: string;
  readonly wifeName: string;
  readonly parents: ReadonlyArray<string>;
}

const user: User = {
  id: 1,
  name: "John",
  wifeName: "Mary",
  parents: ["George", "Maria"]
};

user = {}; // Cannot assign to 'user' because it is a constant.
user.id = 2; // Cannot assign to 'id' because it is a read-only property.
user.wifeName = 'Hermione'; // Cannot assign to 'wifeName' because it is a read-only property.
user.parents = []; // Cannot assign to 'parents' because it is a read-only property.
user.parents.push("Thor"); // Property 'push' does not exist on type 'readonly string[]'.

And that's it, from the help of Typescript service we now get the immutable user object.

*** Update 27/04/2019

type DeepReadonly can be used to change the interface to be the new deep readonly type, it will apply readonly to all properties of the interface. (Deeply)

type DeepReadonlyObject<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> };

type DeepReadonly<T> = T extends (infer E)[][]
  ? ReadonlyArray<ReadonlyArray<DeepReadonlyObject<E>>>
  : T extends (infer E)[]
  ? ReadonlyArray<DeepReadonlyObject<E>>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface ITodo {
  task: string;
  done: boolean;
}

interface IRootState {
  userId: string;
  showCompletedOnly: boolean;
  todoTypes: string[];
  todos: ITodo[];
  iconGrid: string[][];
}

type ReadonlyRootState = DeepReadonly<IRootState>;

let rootState: ReadonlyRootState; // Typescript service will prevent this variable from mutation


Thank you for checking this article hope you guys enjoyed it.

Krish <Dev /> © 2020