Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DeepReadonly Object Typescript

It is possible to create a DeepReadonly type like this:

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

interface A {
  B: { C: number; };
  D: { E: number; }[];
}

const myDeepReadonlyObject: DeepReadonly<A> = {
  B: { C: 1 },
  D: [ { E: 2 } ],
}

myDeepReadonlyObject.B = { C: 2 }; // error :)
myDeepReadonlyObject.B.C = 2; // error :)

This is great. Both B and B.C are readonly. When I try to modify D however...

// I'd like this to be an error
myDeepReadonlyObject.D[0] = { E: 3 }; // no error :(

How should I write DeepReadonly so that nested arrays are readonly as well?

like image 377
Braden Snell Avatar asked Jan 26 '17 17:01

Braden Snell


4 Answers

As of TypeScript 2.8, this is now possible and actually an example in the PR for Conditional Types: https://github.com/Microsoft/TypeScript/pull/21316

Also see the notes on type inference for Conditional Types: https://github.com/Microsoft/TypeScript/pull/21496

I modified the example slightly to use the type inference for the readonly array value type because I find (infer R)[] clearer than Array<T[number]> but both syntaxes work. I also removed the example NonFunctionPropertyNames bit as I want to preserve functions in my output.

type DeepReadonly<T> =
    T extends (infer R)[] ? DeepReadonlyArray<R> :
    T extends Function ? T :
    T extends object ? DeepReadonlyObject<T> :
    T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

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

Doing DeepReadonly this way also preserves optional fields (thanks to Mariusz for letting me know), e.g.:

interface A {
    x?: number;
    y: number;
}

type RA = DeepReadonly<A>;

// RA is effectively typed as such:
interface RA {
    readonly x?: number;
    readonly y: number;
}

While TS still has some easy ways to lose "readonly-ness" in certain scenarios, this is as close to a C/C++ style const value as you will get.

like image 109
zenmumbler Avatar answered Nov 13 '22 11:11

zenmumbler


You might want to use ts-essentials package for that:

import { DeepReadonly } from "ts-essentials";

const myDeepReadonlyObject: DeepReadonly<A> = {
  B: { C: 1 },
  D: [ { E: 2 } ],
}
like image 44
Krzysztof Kaczor Avatar answered Nov 13 '22 10:11

Krzysztof Kaczor


In addition to zenmumbler answer, since TypeScript 3.7 is released, recursive type aliases are now supported and it allows us to improve the solution:

type ImmutablePrimitive = undefined | null | boolean | string | number | Function;

export type Immutable<T> =
    T extends ImmutablePrimitive ? T :
    T extends Array<infer U> ? ImmutableArray<U> :
    T extends Map<infer K, infer V> ? ImmutableMap<K, V> :
    T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;

export type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
export type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };

You may notice that instead of extending the base interfaces, as the old solution does, like interface ImmutableArray<T> extends ReadonlyArray<Immutable<T>> {}, we refer them directly like type ImmutableArray<T> = ReadonlyArray<Immutable<T>>.

The old solution works pretty well in most cases, but there are few problems because of replacing original types. For example, if you use immer and pass the old implementation of ImmutableArray to the produce function, the draft will lack of array methods like push().

There is also the issue on GitHub about adding DeepReadonly type to TypeScript.

like image 20
Valeriy Katkov Avatar answered Nov 13 '22 11:11

Valeriy Katkov


I think this is a better solution:

type DeepReadonly<T> = {
    readonly [P in keyof T]: DeepReadonly<T[P]>
}
like image 4
Dmytro Shyryayev Avatar answered Nov 13 '22 11:11

Dmytro Shyryayev