Challenge

Implement a generic DeepReadonly<T> which makes every parameter of an object and its sub-objects readonly recursively.

You can assume that we are only dealing with Objects in this challenge. Arrays, Functions, Classes and so on are no need to take into consideration. However, you can still challenge yourself by covering different cases as many as possbile.

For example:

type X = {
  x: {
    a: 1;
    b: "hi";
  };
  y: "hey";
};

type Expected = {
  readonly x: {
    readonly a: 1;
    readonly b: "hi";
  };
  readonly y: "hey";
};

const todo: DeepReadonly<X>; // should be same as `Expected`

Solution

In this challenge, we need to create the same Readonly<T> type. The only difference is that we need to make it recursive.

Let us start from classic and implement the regular Readonly<T> type:

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

But, as you already aware, this type will not make everything read-only, but only the fields that are not in depth. The reason is that when our T[P] is not a primitive, but an object, it will just pass it as is, without making its properties read-only.

Therefore, we need to replace T[P] with a recursive usage of DeepReadonly<T>. Do not forget about basic case when using recursions, though.

The algorithm is simple. In case T[P] is an object, we are going deeper into DeepReadonly, otherwise - return T[P]:

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends Record<string, unknown>
    ? DeepReadonly<T[P]>
    : T[P];
};

References