본문 바로가기
Language/Typescript

타입챌린지 : 12-Chainable Options (medium)

by hsloth 2023. 3. 21.

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

 

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

 

일단, 테스트 케이스를 봐보자.

// test case
type Chainable = {
  option(key: string, value: any): any
  get(): any
}

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
}

Step 1.

a.option().option() 이런 식으로 option이라는 속성을 연속적으로 사용하기 위해서는 이전 option의 반환 타입이 Chainable 이어야 함을 의미한다.

ex) const k = a.option(); 일 때, k의 타입이 Chainable이라면, 이어서 k.option() 으로 코드를 작성할 수 있고 이는 곧 a.option()의 반환 타입이 Chainable이라는 뜻이고, a.option().option() 이 가능하다는 뜻이 된다.

따라서, Chainable에서 option의 속성의 반환 타입은 Chainable 타입이어야 한다는게 첫번째 조건이다.

그래서 아래와 같이 코드를 일단 짜볼 수 있다.

type Chainable = {
  option(key: string, value: any): Chainable;
  get(): any;
}

 

Step 2.

여기서 option을 key의 중복 없이, 연속적으로(chainable 하게) 사용하기 위해서는 전 option의 리턴 값이 객체에 대한 정보를 가지고 있어야 한다. 이것을 타입레벨에서 가능하게 하려면 제네릭을 사용해야 한다.

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

위의 코드에서 <T = {}> 을 해준 이유는 제네릭을 이용하면서, Chainable을 제네릭 없이 사용할 경우 (a를 제네릭없이 Chainable로만 선언했기 때문에) 에러를 방지하기 위해 기본 객체를 넣어준 것이다.

그리고 제네릭을 option함수에서 설정한다.

option<K extends string, V> : K는 string 타입이고, 제네릭 V도 설정한다.

(key: K extends keyof T ? never : K, value: V) : key는 K가 T의 key값이면 never를 리턴하고(key값의 중복 방지를 위해) 아니면 K를 리턴한다. value는 V를 리턴한다.

Chainable<T & Record<K, V>> : option의 리턴 타입은 Chainable인데, 현재 객체의 정보를 저장하고 넘기기 위해 제네릭을 설정해준다. T & Record<K, V> 로 K:V 객체와 T를 합한 값을 넘겨준다. &을 사용하는건 중복되는 key 값을 하나로 만들기 위해서이다.

 

Step 3.

get() 에 대해서는 아직 잘 모르겠다. 하지만, 처음 코드만 봐서는 아마도 타입을 객체로 바꿔주는 역할을 하는 것 같다.

따라서 코드를 다음과 같이 수정할 수 있다.

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

get(): T 를 해주어 get() 함수를 사용하면 현재 Chainable이 가지고 있는 정보인 T를 반환하게 한다.

 

Step 4.

그런데도 아직 오류가 뜬다... 마지막 result3과 Expect3이 오류가 뜬다... 왤까?

Expected3은 name: number를 가지고 있지만, result3은 option('name', 'another name')을 반환하고 있나보다. result3의 뒤의 option인 option('name', 123)을 반환하도록 만들어보자.

기존에 T에서 현재 K값을 가지고 있는 경우 기존 K값을 제거해주면 된다.

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;
}

Omit<T, K>를 통해 이전에 존재했던 K값을 제거하면 끝!

 

 

이해가 안가면 아래 블로그를 참고해보자.

나도 이글을 참고하면서 글을 적어보았다.

https://bkdragon0228.tistory.com/9

 

Chainable Options

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

bkdragon0228.tistory.com