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. Thereadonly
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:
- 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 usedeepFreeze
:
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 thePerson
type onbill
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.