DeepReadonly Object Typescript
Asked Answered
I

10

50

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?

Inconsequent answered 26/1, 2017 at 17:23 Comment(3)
I'm not getting an error for console.log(myDeepReadonlyObject.D[0]); Which version of typescript are you using?Loraine
I had the "noImplicitAny" flag set in my tsconfig. The question still stands, however. I've updated it to be more clear. Thanks.Inconsequent
For those interested, DeepReadonly is part of ts-essentials package. Check it out: github.com/krzkaczor/ts-essentialsIssacissachar
B
37

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.

Bluegill answered 5/4, 2018 at 10:42 Comment(6)
Nice answer. But when I run your code in TS 1.8.1 after doing type RA = DeepReadonly<A>; the RA type is equivalent (shows in vs code when hovering over with mouse) to interface RA { readonly x?: number | undefined; readonly y: number; } and x can be omitted.Oribel
Thanks! That's strange, I had an error earlier, but I was tweaking the code as I wrote this answer. I will modify the answer to remove that bit. But the result is even better, so that's good. Thanks for letting me know!Bluegill
Thanks! Why is DeepReadonlyArray even necessary? Since every Array is an object, shouldn't DeepReadonlyObject do? Why is the ` T extends Function ? T ` necessary? Lets not remove functions, but couldn't we just leave this condition out?Gudren
@BenCarp While in JS Arrays are indeed just objects with numeric keys, TS has typed Arrays as more strict interfaces and makes them not directly interchangeable. The subscript operators for each are different, etc. The separate clause for functions is there as they are just passed through this type transformation as is, and is needed as otherwise the object test following it will try to transform the function value. It's mostly that the quirks of JS as expressed in TS types are very visible in this type :)Bluegill
oh, btw, for anyone interested, I've codified this interface and some helpers in an NPM package here: npmjs.com/package/@stardazed/deep-readonlyBluegill
Doesn't work at allKotto
D
27

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.

Donielle answered 22/11, 2019 at 12:0 Comment(1)
This doesn't work with union types: const doesntwork: Immutable<['one'|'two', 1 | 2]> = [1, 2];, while the solution from Dmytro Shyryayev doesLoper
I
19

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 } ],
}
Issacissachar answered 15/12, 2018 at 23:33 Comment(0)
A
8

I think this is a better solution:

type DeepReadonly<T> = {
    readonly [P in keyof T]: DeepReadonly<T[P]>
}
Acker answered 24/11, 2021 at 21:36 Comment(3)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Oppen
Can someone explain why this works?Tartary
This works because it recursivly calls the type-'function' on the key-property... there is a pretty neat 5min youtube video explaining pretty much this exact usecase... although his solution is a bit better with an additional check, like type MyReadonly<TInput> = { readonly [Key in keyof TInput]: TInput[Key] extends object ? MyReadonly<TInput[Key]>: TInput[Key]; } youtube.com/watch?v=U1EygIpjAEMManifold
G
2
export type DR<T> = DeepReadonly<T>

type DeepReadonly<T> =
// tslint:disable-next-line: ban-types
    T extends  AnyFunction | Primitive ? T :
    T extends ReadonlyArray<infer R> ? IDRArray<R> :
    T extends ReadonlyMap<infer K, infer V> ? IDRMap<K, V> :
    T extends ReadonlySet<infer ItemType>? ReadonlySetDeep<ItemType>:
    T extends object ? DRObject<T> :
    T


export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint

export type AnyFunction = (...args: any[]) => any

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

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

interface IDRMap<K, V> extends ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> {}

interface ReadonlySetDeep<ItemType>
    extends ReadonlySet<DeepReadonly<ItemType>> {}

DeepReadonly generic is a valuable tool that can help enforce immutability.

  • I use the short DR name since I use this generic so often.
  • T extends ReadonlyArray<infer R> ? will be true for both Array<any> and ReadonlyArray<any>.
Gudren answered 1/5, 2019 at 0:34 Comment(0)
L
1

You can have a readonly array:

interface ReadonlyArray<T> extends Array<T> {
    readonly [n: number]: T;
}
let a = [] as ReadonlyArray<string>;
a[0] = "moo"; // error: Index signature in type 'ReadonlyArray<string>' only permits reading

But you can't use it with your solution:

interface A {
    B: { C: number; };
    D: ReadonlyArray<{ E: number; }>;
}

myDeepReadonlyObject.D[0] = { E: 3 }; // still fine

The type of D is DeepReadonly<ReadonlyArray<{ E: number; }>> and it won't allow the ReadonlyArray to kick in.

I doubt that you'll manage to make it work to objects with arrays in them, you can have either deep read only for arrays or for objects if you want a generic interface/type and not specific ones.
For example, this will work fine:

interface A {
    readonly B: { readonly C: number; };
    D: ReadonlyArray<{ E: number; }>;
}

const myDeepReadonlyObject = {
    B: { C: 1 },
    D: [{ E: 2 }],
} as A;

myDeepReadonlyObject.B = { C: 2 }; // error
myDeepReadonlyObject.B.C = 2; // error
myDeepReadonlyObject1.D[0] = { E: 3 }; // error

But it has a specific interface to it (A) instead of a generic one DeepReadonly.

Another option is to use Immutable.js which comes with a builtin definition file and it's pretty easy to use.

Loraine answered 26/1, 2017 at 23:29 Comment(1)
Thanks. It's a real shame this doesn't appear to be possible yet. Anyone have any idea if it's in the works?Inconsequent
S
1

You can use ts-toolbelt, it can do operations on types at any depth

In your case, it would be:

import {O} from 'ts-toolbelt'

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

type optional = O.Readonly<A, keyof A, 'deep'>

And if you want to compute it deeply (for display purposes), you can use Compute for that

Showery answered 19/6, 2019 at 21:1 Comment(0)
R
1
type DeepReadonly<T> = {
    readonly [Key in keyof T]: T[Key] extends any[] | Record<string, unknown> ? DeepReadonly<T[Key]> : T[Key]
}
Recidivism answered 23/5, 2023 at 7:45 Comment(1)
I recommend that you don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes.Melar
D
0

There is a suggestion issue open in the Typescript repository asking for a definition of this type.

As the thread is very long and has many interesting contributions and implementations, I am going to copy my answer that summarizes them and ends with a proposed implementation:


I have tried to compile all the contributions of this issue:

  • (comment) @Dean177 proposes the first implementation
  • (comment) When asked by @esamattis about whether it works with Arrays, @Dean177 realizes that it does not and corrects his implementation.
  • (comment) @RomkeVdMeulen makes a clear effort to exclude methods. I don't know if when he made that comment it was necessary, but at least now with the latest version of TS I can confirm that using readonly on objects does not allow you to use methods.
  • (comment) @nieltg modifies @Dean177's implementation to include maps and Function, and proposes calling the type DeepImmutable instead of DeepReadOnly to avoid collisions/confusion (I think it's a good idea)
  • (comment) @paps proposes a modification to "handle unknown". In my opinion, this comment can be dismissed, since the code doesn't actually do anything.
  • (comment) @carpben says that as of TS 3.4, object readonly should also work for arrays, but for some reason it doesn't, and it's still necessary to use ReadonlyArray
  • (comment) @carpben proposes checking if T extends object and leaving the primitives as fallback instead of the other way around, as it results in a cleaner and more compact syntax (I agree)
  • (comment) @icesmith has a comment with quite a few upvotes, but while he seems to be "correcting" the proposals so far, he basically says that (1) in TS 3.7 you can do type A = type B instead of interface A extends B {}, and (2) then mentions something about immer that has nothing to do with the discussion of this issue.
  • (comment) @xenon is the first to mention Sets. I don't know why no one mentioned it before as it was done with maps or arrays. It's a nice addition, as it allows reading methods like has to be used while prohibiting write methods like add.
  • (comment) @Offirmo notes that PR #26063 now improves Readonly so that it is no longer necessary to handle arrays separately. In fact, @offirmo seems to suggest that not only is it not necessary, but it is preferable since it can cause problems. However, I don't understand his example very well, or at least I couldn't reproduce it in the playground. Anyway, I have verified that everything works fine without using ReadOnlyArray, (that is, mutation methods like push are prohibited but read methods like at or find are allowed).

Based on all this, my proposal is the following:

type DeepImmutable<T> =
  T extends Map<infer K, infer V>
    ? ReadonlyMap<DeepImmutable<K>, DeepImmutable<V>>
    : T extends Set<infer S>
      ? ReadonlySet<DeepImmutable<S>>
      : T extends object
        ? { readonly [K in keyof T]: DeepImmutable<T[K]> }
        : T;

Optional: if you like it, you can extract ImmutableMap, ImmutableSet and ImmutableObject as separate types.

Dougald answered 26/6 at 14:44 Comment(0)
C
-1

Now you can just use as const as it makes readonly for all nested objects

According to https://github.com/microsoft/TypeScript/issues/31856

here is an example https://www.typescriptlang.org/play?#code/MYewdgzgLgBAhjAvDA3gKBpmY4FsCmAXDAIwA0GWcA5kapVljgceQ4-LcQEwUdYATOFDjEA2gF12AXzTT4EGKEhQ0auADoa+DUJEaADgFcIACwAUJAJQBuNJu0bm+JDADMdoA

Contrapose answered 13/4, 2022 at 15:25 Comment(1)
as const is for literals, but the question is about generic types.Zigzag

© 2022 - 2024 — McMap. All rights reserved.