iamkanguk.dev

[Type Challenge - MEDIUM] Chainable Options 본문

Type Challenge

[Type Challenge - MEDIUM] Chainable Options

iamkanguk 2024. 10. 2. 15:44

https://github.com/type-challenges/type-challenges/blob/main/questions/00012-medium-chainable-options/README.md

 

type-challenges/questions/00012-medium-chainable-options/README.md at main · type-challenges/type-challenges

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

github.com

 

타입스크립트 스터디도 진행하면서 타입에 조금 더 자신이 생겼다고 생각했지만 이 문제를 보자마자 이해 자체를 하지 못했다.

그래서 다른 분들이 어떤 생각을 가지고 풀이를 했는지를 먼저 파악하고, 직접 생각하면서 풀려고 노력을 해봤다.

시작을 어떻게 해야할지만 파악한다면 크게 어렵지는 않은 문제였던 것 같은데, 시작을 어떻게 해야할지 파악하는게 가장 어려웠던 것 같다.

 

해당 글은 다른 분들의 풀이를 참고했습니다!

https://suloth.tistory.com/52

 

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

이 글은 제가 타입챌린지를 하면서 해석한 내용을 적는 글입니다. 틀린 내용이 있으면 댓글 달아주시면 감사하겠습니다. https://github.com/type-challenges/type-challenges/blob/main/questions/00012-medium-chainable-o

suloth.tistory.com

 

1단계: option 메서드가 Chainable을 반환한다는 점을 파악해야 한다.

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

declare const a: Chainable;

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

 

위의 소스코드를 보면 Chainable이라는 타입을 가지는 a라는 변수에 option 메서드를 체이닝 하면서 적용하고 있다.

즉, option이라는 메서드가 Chainable을 반환을 해야 동작이 가능한 소스코드라는 점이다.

 

a.option('foo', 123) 이라는 코드가 Chainable 타입의 데이터를 반환해야, 반환된 Chainable 타입의 데이터에서 option 메서드를 추가로 사용할 수 있기 때문이다.

 

2단계: 상태 유지를 위해서는 제네릭을 사용해야 한다.

option 메서드가 Chainable을 반환하면서 체이닝이 되어야 한다. 그러기 위해서는 option 메서드가 반환하는 값이 객체에 대한 정보를 가지고 있어야 할 것이다. 

 

1단계에서 보여지는 result1을 보면 foo라는 키에는 123의 타입인 number가 할당되어야 하고 두 번째 option 메서드가 사용되는 시점에는 1단계에서 사용된 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;
}

 

 

- Chainable<T = {}>제네릭을 통해 처음 기본 객체(빈 객체)를 선언해주어야 체이닝이 가능하다.

- option<K extends string, V> : K는 string 타입이고, V도 선언해준다.

- (key: K extends keyof T ? never : K, value: V) : key는 K가 T의 key 값이면 중복 방지를 위해 never를 반환하고, 아니면 K를 그대로 적용한다. value는 제네릭 V를 그대로 적용해준다.

- Chainable<T & Record<K, V>> : 현재 객체의 정보를 저장하고 넘기기 위해서 제네릭을 설정해주는 것이다. T에다가 매개변수로 받은 K와 V 조합을 T에다가 적용을 시켜줘서 반환한다는 의미이다.

 

3단계: get 메서드의 리턴 타입은 무엇일까?

필자는 처음 구현할 때 ReturnType 뭐시기 해서 사용했었는데 그럴 필요가 없었다. 그냥 T 그대로 리턴하면 되는 것이다.

왜냐하면 결국에 get은 option 체이닝을 진행하고 마지막에 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
}

 

4단계: 마지막 오류 해결

위의 코드까지 적용해보면 마지막 테스트 케이스에만 오류가 발생하는데 이유를 확인해보니까..

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

type Expected3 = {
  name: number
}

 

위와 같이 name 이라는 같은 키에서 처음에는 string을, 그다음에는 123이라는 number 타입을 적용했다.

Expected3 에는 name이라는 키에는 number 타입이 적용되었다. 즉, 마지막으로 입력된 값이 남는다는 것이다.

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
}

 

그래서 option 메서드의 반환 타입을 수정했는데 T에서 K 값을 가지고 있으면 기존 K 값을 제거해주고 새로운 K 값을 넣어주면 된다.

해당 로직을 적용하기 위해서는 Omit을 사용하면 될 것 같다.