sajad torkamani

Solution

type IsPrimitive<T> = keyof T extends never ? true : false

type DeepReadonly<T> = {
  readonly [P in keyof T]: IsPrimitive<T[P]> extends true
    ? T[P]
    : DeepReadonly<T[P]>
}

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<DeepReadonly<X1>, Expected1>>,
  Expect<Equal<DeepReadonly<X2>, Expected2>>,
]

type X1 = {
  a: () => 22
  b: string
  c: {
    d: boolean
    e: {
      g: {
        h: {
          i: true
          j: 'string'
        }
        k: 'hello'
      }
      l: [
        'hi',
        {
          m: ['hey']
        },
      ]
    }
  }
}

type X2 = { a: string } | { b: number }

type Expected1 = {
  readonly a: () => 22
  readonly b: string
  readonly c: {
    readonly d: boolean
    readonly e: {
      readonly g: {
        readonly h: {
          readonly i: true
          readonly j: 'string'
        }
        readonly k: 'hello'
      }
      readonly l: readonly [
        'hi',
        {
          readonly m: readonly ['hey']
        },
      ]
    }
  }
}

type Expected2 = { readonly a: string } | { readonly b: number }

TypeScript playground

How it works

We create the new type using mapped types by mapping over the keys of T with [P in keyof T].

When determining the value of the key, we check if it’s a primitive type:

  • If it is a primitive, we extract its type with T[P].
  • If it’s not a primitive (i.e., it’s an object type), we recursively call the DeepReadonly generic again and pass the current property to it (DeepReadonly<T[P]> to extract the type of each property.
Tagged: TypeScript