The image is how NightCafe sees deep readonly type.
The link to the playground for this article is down below.
Immutability could be useful for certain cases when building applications, and today we will take a look how to enforce immutability using Typescript.
Normally to get an immutable object, it would need to be frozen with the appropriate JavaScript function, but that would only freeze a single nesting level of it and would not affect anything nested inside, whether in its objects inside properties or arrays of objects.
A possible solution would be to iterate over all properties recursively, freezing all objects and repeating the process for any nested arrays of objects.
However, Typescript provides means that can potentially eliminate this need if used wisely.
Suppose we have the following interfaces representing a family:
interface Person {
firstName: string;
lastName: string;
age: number;
children?: Person[];
}
interface Family {
parents: Person[];
grandparents?: {
paternal: Person[];
maternal: Person[];
};
}
And a corresponding variable representing the family instance:
const family: Family = {
parents: [
{ firstName: "John", lastName: "Doe", age: 40 },
{ firstName: "Jane", lastName: "Doe", age: 38 },
],
grandparents: {
paternal: [
{ firstName: "PaternalGrandfather", lastName: "Doe", age: 70 },
{ firstName: "PaternalGrandmother", lastName: "Doe", age: 68 },
],
maternal: [
{ firstName: "MaternalGrandfather", lastName: "Smith", age: 75 },
{ firstName: "MaternalGrandmother", lastName: "Smith", age: 72 },
],
}
};
In order to provide means of immutability, we could write a generic, which recursively traverses and interface field and marks everything it encounters as Readonly
, essentially mimicking the freezing, but eliminating the fuss of the actual deep freeze.
type DeepReadonly<T> = Readonly<{
[K in keyof T]:
// Is it a primitive? Then make it readonly
T[K] extends (number | string | symbol) ? Readonly<T[K]>
// Is it an array of items? Then make the array readonly and the item as well
: T[K] extends Array<infer A> ? Readonly<Array<DeepReadonly<A>>>
// It is some other object, make it readonly as well
: DeepReadonly<T[K]>;
}>
There, now we can create objects, which can be real constants:
const family2: DeepReadonly<Family> = {
parents: [
{ firstName: "John", lastName: "Doe", age: 40 },
{ firstName: "Jane", lastName: "Doe", age: 38 },
],
grandparents: {
paternal: [
{ firstName: "PaternalGrandfather", lastName: "Doe", age: 70 },
{ firstName: "PaternalGrandmother", lastName: "Doe", age: 68 },
],
maternal: [
{ firstName: "MaternalGrandfather", lastName: "Smith", age: 75 },
{ firstName: "MaternalGrandmother", lastName: "Smith", age: 72 },
],
}
};
Any changes to the object typed with the generic are going to be stopped by the compiler:
family.parents = []; // ok
family2.parents = []; // error
family.parents[0].age = 1; // ok
family2.parents[0].age = 1; // error
// ok
family.parents.push({
age: 40,
firstName: 'Joseph',
lastName: 'Doe'
});
// error
family2.parents.push({
age: 40,
firstName: 'Joseph',
lastName: 'Doe'
});
All benefits from Object.freeze
without a single freeze, cool, eh? At this point you are probably wondering how to shoot yourself in the foot with it, there should be a way.
And there is a way indeed, shooting in the foot is possible using reference types:
const family3: DeepReadonly<Family> = family;
As you remember, family
is just Family
, so any changes to it would mutate family3
, even though it is deep readonly.
This is the way things are.
Hope you enjoyed the article as much as I did while researching this :)
The playground.
P.S. if someone knows how to pull this trick with generics in JSDoc, please post it in the comments :)
P.P.S. JSDoc conversion by @artxe2 so I don't lose it.