Learn TypeScript

Creating deep immutable types

Creating deep immutable types

In this lesson, we will learn various ways of making types deeply immutable.

Using const assertions

A const assertion is a kind of type assertion where the keyword const is used in place of the type:

let variableName = someValue as const;

The const assertion results in TypeScript giving the variable an immutable type based on the value structure. Here are some key points on how TypeScript infers the type from a const assertion:

  • For objects, all its properties will have the readonly modifier. The readonly modifier is recursively applied to all nested properties.

  • The readonly modifier is applied to arrays. The type will also be a fixed tuple of specific literal values in the array.

  • Primitives will be a literal type of the specific value of the primitive.

We are going to explore const assertions in the TypeScript playground.

  • Open the TypeScript playground by clicking the link below:
Open TypeScript Playground
  • Paste the code from below into the TypeScript Playground:
const bill = {
name: "Bill",
profile: {
level: 1,
},
scores: [90, 65, 80],
};
bill.name = "Bob";
bill.profile.level = 2;
bill.scores.push(100);

No type errors occur at the moment because bill is mutable.

  • Make bill immutable with a const assertion.
🤔

What is the type given to bill now?

🤔

Are any type errors raised on the assignments?

Nice!

Creating a deepFreeze function

The const assertion gives us deep immutability at compile time. We can achieve runtime deep immutability by creating and using a deepFreeze function based on the Object.freeze JavaScript method.

  • First, let's verify that our code at the moment isn't runtime immutable. Output bill to the console at the end of the program:
console.log(bill);
  • Open the console and click the Run option.

We will see that bill has been mutated.

  • Add the following deepFreeze function:
function deepFreeze<T>(obj: T) {
var propNames = Object.getOwnPropertyNames(obj);
for (let name of propNames) {
let value = (obj as any)[name];
if (value && typeof value === "object") {
deepFreeze(value);
}
}
return Object.freeze(obj);
}

The function recursively applies Object.freeze to all the properties in an object.

  • Change bill to use deepFreeze:
const bill = deepFreeze({
name: "Bill",
profile: {
level: 1,
},
scores: [90, 65, 80],
} as const);

We still get type errors, which is great.

  • Click the Run option.
🤔

Has bill been mutated?

Neat!

Creating a DeepImmutable type

At the moment, the type of bill is inferred. What if we want to use a type annotation on bill explicitly? Does this mean we have to construct a type with all the necessary readonly modifiers? Fortunately, we can create a utility type to help us.

  • First, let's create a type without any readonly modifiers:
type Person = {
name: string;
profile: {
level: number;
};
scores: number[];
};
  • We can create the following utility type to make a type immutable. The type recursively sets all properties on the type parameter to be readonly:
type Immutable<T> = {
readonly [K in keyof T]: Immutable<T[K]>;
};
  • Let's use the Immutable type with the Person type on bill in a type annotation. Let's also remove the const assertion:
const bill: Immutable<Person> = deepFreeze({
name: "Bill",
profile: {
level: 1,
},
scores: [90, 65, 80],
});
🤔

Are type errors raised on all the assignments still?

Nice!

Summary

A const assertion is a convenient way of making an object or array deeply immutable at compile-time.

A function that recursively leverages Object-freeze can make an object or array deeply immutable at runtime.

A deep immutable mapped type can be created to recursively add the readonly modifier to all the properties in the type.

© 2024 Carl Rippon
Privacy Policy
This site uses cookies. Click here to find out more