이 글은 제가 타입챌린지를 하면서 해석한 내용을 적는 글입니다. 틀린 내용이 있으면 댓글 달아주시면 감사하겠습니다.
일단, 테스트 케이스를 봐보자.
// 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
'Language > Typescript' 카테고리의 다른 글
타입챌린지 : 16-Pop (medium) (0) | 2023.03.22 |
---|---|
타입챌린지 : 15-Last of Array (medium) (0) | 2023.03.22 |
타입챌린지 : 10-Tuple To Union (medium) (0) | 2023.03.18 |
타입챌린지 : 9-Deep Readonly (medium) (0) | 2023.03.16 |
타입챌린지 : 8-Readonly 2 (medium) (0) | 2023.03.12 |