본문 바로가기
Language/Typescript

타입챌린지 : 9-Deep Readonly (medium)

by hsloth 2023. 3. 16.

 

이 글은 제가 타입챌린지를 하면서 해석한 내용을 적는 글입니다. 틀린 내용이 있으면 댓글 달아주시면 감사하겠습니다.

 

https://github.com/type-challenges/type-challenges/blob/main/questions/00009-medium-deep-readonly/README.md

 

GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

Collection of TypeScript type challenges with online judge - GitHub - type-challenges/type-challenges: Collection of TypeScript type challenges with online judge

github.com

 

 

// 정답 코드
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends Record<string, any> ? 
  T[P] extends Function ?
  T[P] :
  DeepReadonly<T[P]> :
  T[P]
}

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

// test case
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 }

 

맨 처음 나의 코드는 이러했다.

// 틀린 코드
type DeepReadonly<T> = {
  readonly [P in keyof T as P extends Record<string, unknown> ? DeepReadonly<P> : P]: T[P]
}

멍청하게 key값에다가 Record를 상속받는지 체크하고 있었다... P는 key값이고 T[P]가 value값이라 T[P]를 체크했어야하는데...

따라서 위의 답은 당연히 오답이다.

 

다음 나의 코드를 봐보자.

// 틀린 코드
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends Record<string, unknown> ? 
  DeepReadonly<T[P]> :
  T[P]
}

value값인 T[P]가 객체형태면, 재귀혀태로 한 번 더 타입 검사를하고 아니면 T[P]를 그대로 리턴한다.

이 답도 틀렸다. 왜냐하면 value값으로 함수가 들어갈 경우, 함수는 name: string을 속성으로 가지고 있어서 Record<string, unknown>이라는 조건을 만족하기 때문에 재귀를 타버리게 되어 에러가 나게 된다.

 

그래서 한 번 더 체크를 해주었다.

// 틀린 코드
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends Record<string, unknown> ? 
  T[P] extends Function ?
  T[P] :
  DeepReadonly<T[P]> :
  T[P]
}

T[P]가 Record형태일 때, T[P]가 함수이면 재귀를 타지 않도록 삼항 연산자를 한 번 더 추가해주었다. 그런데 여전히 에러가 난다... 왜일까 찾아봤더니 바로 unknown이 문제였다. unknown은 컴파일 시 타입을 구체적으로 지정하지 않으면 에러를 뱉는데 아무래도 그래서 안되는 것 같았다.

 

그래서 unknown을 any로 바꿔주었더니 에러를 뱉지 않았다.

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

readonly [P in keyof T] : T[P] extends Record<string, any> ? : T의 키값을 P라고 하고 value값은 T[P]를 타입으로 가지는데, value값 T[P] (value == T[P]라고 생각하자)가 Record(객체) 형태일 때,

T[P] extends Function ? T[P] : DeepReadonly<T[P]> : T[P] : T[P]가 함수이면, T[P](value)를 리턴하고, 함수가 아니면 재귀를 실행시키며, Record 형태가 아니라면 T[P]를 리턴한다.

 

그리고 두 번째 답에 대한 해설이다.

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

 

readonly [P in keyof T]: keyof T[P] extends never ? : T의 키값을 P라고 하자. 그리고 value값 T[P]의 키값이 없다면

T[P] : DeepReadonly<T[P]> : T[P]를 그대로 리턴하고 아니라면 value값인 T[P]로 재귀를 실행한다.