44

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?

Braden Snell
  • 1,171
  • 2
  • 10
  • 16
  • I'm not getting an error for `console.log(myDeepReadonlyObject.D[0]);` Which version of typescript are you using? – Nitzan Tomer Jan 26 '17 at 18:02
  • I had the "noImplicitAny" flag set in my tsconfig. The question still stands, however. I've updated it to be more clear. Thanks. – Braden Snell Jan 26 '17 at 18:10
  • 2
    For those interested, `DeepReadonly` is part of `ts-essentials` package. Check it out: https://github.com/krzkaczor/ts-essentials – Krzysztof Kaczor Dec 15 '18 at 23:29

9 Answers9

34

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.

zenmumbler
  • 586
  • 5
  • 12
25

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.

Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123
17

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 } ],
}
Krzysztof Kaczor
  • 5,408
  • 7
  • 39
  • 47
5

I think this is a better solution:

type DeepReadonly<T> = {
    readonly [P in keyof T]: DeepReadonly<T[P]>
}
  • 2
    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](/help/how-to-answer). – Community Nov 25 '21 at 00:13
  • Can someone explain why this works? – James Aug 18 '22 at 13:00
  • 1
    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 = { readonly [Key in keyof TInput]: TInput[Key] extends object ? MyReadonly: TInput[Key]; } https://www.youtube.com/watch?v=U1EygIpjAEM – Florian Oct 06 '22 at 15:29
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>.
Ben Carp
  • 24,214
  • 9
  • 60
  • 72
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.

Nitzan Tomer
  • 155,636
  • 47
  • 315
  • 299
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

millsp
  • 1,259
  • 1
  • 10
  • 23
0
type DeepReadonly<T> = {
    readonly [Key in keyof T]: T[Key] extends any[] | Record<string, unknown> ? DeepReadonly<T[Key]> : T[Key]
}
  • 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. – Mark Rotteveel May 23 '23 at 12:02
-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

Vasiliy vvscode Vanchuk
  • 7,007
  • 2
  • 21
  • 44