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.
Top comments (7)
Your code works incorrectly for tuple types:
Apparently it's the inferring the array type and passing it back to the
Array
generic that messes up the case for tuples. Turns out this step can be avoided by passing the property type directly, then the generic gets even smaller:Here's the playground with the tuple example you provided.
I don't get, what's the benefit over this simpler variant?
I don't see any apparent advantages, this one seems less verbose 🤔
That's a good observation, I haven't accounted for the tuples when designing it, thanks 👍
Perhaps, I'll revise it 🤓
The
Readonly<T[K]>
for primitives is unnecessary because those types don't have any mutable structure - you can't edit what is stored inside a number, string or symbol object. (Also you missed outboolean
.) For all primitives you can just map toT[K
].You're already making the properties (that hold these values) read-only by wrapping the whole type in
Readonly
on the first line, so that's enough.(If you wrap them with
Readonly
unnecessarily then TS can generate spurious type errors.)Excellent point, thanks Daniel! 😁