bkdragon's log

Chainable Options 본문

Typescript

Chainable Options

bkdragon 2023. 3. 14. 23:26

타입챌린지  Chainable Options 문제를 풀어보자. 단순한 문제 풀이로만 글을 쓰고 싶지 않은데, 이 문제는 답을 이해하는 것도 꽤나 어려웠다. 고민하면서 얻은 노하우와 같이 설명해보겠다. 아래는 문제의 링크이다.

 

https://github.com/type-challenges/type-challenges/blob/main/questions/00012-medium-chainable-options/README.ko.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

 

우선 문제의 감을 잡는데는 테스트 케이스를 보는것이 꽤나 도움이 된다. 

 

import type { Alike, Expect } from '@type-challenges/utils'

declare const a: Chainable

const result1 = a
  .option('foo', 123)
  .option('bar', { value: 'Hello World' })
  .option('name', 'type-challenges')
  .get()

const result2 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 'last name')
  .get()

const result3 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 123)
  .get()

type cases = [
  Expect<Alike<typeof result1, Expected1>>,
  Expect<Alike<typeof result2, Expected2>>,
  Expect<Alike<typeof result3, Expected3>>,
]

type Expected1 = {
  foo: number
  bar: {
    value: string
  }
  name: string
}

type Expected2 = {
  name: string
}

type Expected3 = {
  name: number
}

처음 눈에 들어오는 것은 Chainable 타입에 제너릭이 없다는 점이다. 기본값을 줘야할 수 있다는 힌트가 된다.

다음은 에러가 발생하는 부분이다. 같은 키값을 넣으면 에러가 발생한다. 그리고 나중에 입력된 값만이 남는 것도 확인 할 수 있다(Expected3).

 

문제와 별개로 이 두가지 정보를 테스트 케이스에서 얻을 수 있다.

 

처음 얻은 정보를 통해 제네릭에 기본값을 줘야될 수 있다는 것을 알았다. 문제에서 다루는 자료구조가 객체이기 때문에 다음과 같이 줄 수 있다.

type Chainable<T ={}> = {
  option(key: string, value: any): any
  get(): any
}

 

 

다음 정보는 기존에 객체에 포함되는 키 값을 줄 수 없다는 점이다. option 함수에 제네릭을 줘서 구현할 수 있다.

type Chainable<T ={}> = {
  option<K extends string, V>(key: K extends keyof T ? never : K, value: V): any
  get(): any
}

 

 

이제 option의 반환값에 대해 고민해보자. option을 체이닝 할수록  만들고 있는 객체가 점점 완성(정확히는 key와 value가  쌓여간다.)이 되어간다. 그리고 그 객체 역시 option을 가지고 있어야한다 (get하기 전까진). 처음 값에서 새로운 key와 value가 합쳐진 객체이면서 option을 가지는, 즉 Chainable에 key와 value가 추가된 타입을 제네릭으로 받는 형태를 반환해야한다.

 

아래는 기존의 T에 새로운 key value로 만든 객체를 합친 타입을 제네릭으로 갖는 Chinable을 반환한다.

type Chainable<T ={}> = {
  option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<T & Record<K, V>>
  get(): any
}

 

 

get은 간단하다. 현재 T를 반환하면 된다.

type Chainable<T ={}> = {
  option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<T & Record<K, V>>
  get(): T
}

 

 여기까지 하면 테스트 중 마지막 테스트만 오류가 발생한다. 처음에 테스트 케이스를 보며 얻은 정보 중에 나중에 입력된 값이 남는다는 것을 알았다. 그럼 기존의 객체에서 option의 key와 같은 값은 뺴줘야한다.

 

type Chainable<T ={}> = {
  option<K extends string, V>(key: K extends keyof T ? never : K, value: V): Chainable<Omit<T, K> & Record<K, V>>
  get(): T
}

모든 오류가 사라졌다.

 

 

타입 레벨에서 기존에 존재하는 key와 같은 값을 받지 못하게 했지만 실제로 그것은 별개의 문제가 되는 것 같다. 타입을 선언하는 부분과 테스트 케이스를 잘 살펴보며 문제를 풀어보자.

'Typescript' 카테고리의 다른 글

Template Literal Types  (0) 2023.04.04
Conditional types  (0) 2023.03.22
Deep Readonly  (0) 2023.03.10
key in keyof T as key extends K ? never : key ???  (0) 2023.03.03
T[number] 란?  (0) 2023.03.02