이 글은 제가 타입챌린지를 하면서 해석한 내용을 적는 글입니다. 틀린 내용이 있으면 댓글 달아주시면 감사하겠습니다.
와.. 좀 많이 어려운 문제였다.
배열을 PromiseAll함수의 인자로 받아서 Promise<배열>을 리턴하는 함수이다.
type MyAwaited<T> = T extends PromiseLike<infer R> ? MyAwaited<R> : T
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{ [K in keyof T]: MyAwaited<T[K]>}>
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
const promiseAllTest1 = PromiseAll([1, 2, 3] as const)
const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])
const promiseAllTest4 = PromiseAll<Array<number | Promise<number>>>([1, 2, 3])
const a = [1,2,3]
type cases = [
Expect<Equal<typeof promiseAllTest1, Promise<[1, 2, 3]>>>,
Expect<Equal<typeof promiseAllTest2, Promise<[1, 2, number]>>>,
Expect<Equal<typeof promiseAllTest3, Promise<[number, number, number]>>>,
Expect<Equal<typeof promiseAllTest4, Promise<number[]>>>,
]
내가 처음으로 생각한 답을 봐보자.
declare function PromiseAll<T>(values: any): Promise<T>;
정말 하찮다. 어떻게 풀어야할지 모르겠어서 일단 되는대로 적었다. 그래도 얼추 틀은 맞았다. 여기서 몇가지만 추가해주면 된다.
일단, 저놈의 T를 어떻게 해야된다. 그래서 T를 끼워 맞추기 위해 다음과 같이 작성했다.
declare function PromiseAll<T extends any[]>(values: T): Promise<T>
PromiseAll은 배열을 인자로 받기 때문에 T를 배열이라고 제한하고, values를 T타입으로 명시해 주었다.
여기서 문제는 as const로 인자를 타입으로 받으면 에러가 난다는 것이다... ( [1,2,3] as const 는 [1,2,3] 자체를 타입으로 정하는 문법?이다)
그래서 아래와 같이 수정해주었다.
declare function PromiseAll<T extends any[]>(values: readonly T): Promise<T>
이번엔 values: readonly T에서 에러가 발생하기 시작했다. 이유를 알아보니... T는 그냥 any[] 타입이라 readonly를 설정할 수 없다는 것이었다. readonly는 배열 및 튜플의 리터럴에만 허용이 된단다... 쉽게 말하자면... T는 any[] 타입이라 그 자체로 타입이기 때문에 readonly 설정이 안되고, [...T] 같이 만들어 주어야 readonly를 설정해줄 수 있다는 것이다.
그래서 아래와 같이 코드를 수정했다.
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<T>
여기서, Promise<T>를 그대로 리턴하면 안된다. 왜냐하면 함수의 인자로 Promise가 들어갔을 경우, Promise 안에 들어있는 값을 꺼내주어야 하기 때문이다.
ex) 테스트 케이스의 Promise.resolve(3)의 경우 Promise를 리턴하기 때문. Promise<[1, 2, Promise]> 를 리턴하면 안되니까...
그래서 아래와 같이 코드를 고칠 수 있다.
type MyAwaited<T> = T extends PromiseLike<infer R> ? MyAwaited<R> : T;
declare function PromiseAll<T extends any[]>(values: readonly [...T])
: Promise<{ [K in keyof T]: MyAwaited<T[K]>}>;
타입스크립트에서 배열은 key값이 0, 1, 2... 인 객체기 때문에
[K in keyof T] 로 T의 키값인 0, 1, 2... 를 빼내고, 해당 K의 value를 MyAwaited<T[K]>로 정의한다.
MyAwaited는 Promise를 제거하는 타입이다. (Promise안에 들어있는 값을 빼내는 타입) 여기서 T[K]는 T배열에서 K라는 key의 value값을 뜻한다.
그리고 이걸 중괄호로 감싸서 해당 값이 객체라고 말해준다.
즉, T가 [1, 2, 3] 이라면
{ 0: 1, 1: 2, 2: 3} 인 객체가 되는 것이다.
MyAwaited 구현은 아래를 참고하자.
참고
easy난이도에서 MyAwaited 구현
의문점
그렇다면 promiseAllTest2와 promiseAllTest3의 차이점이 뭐길래 리턴 값이 다른걸까?
const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])
Expect<Equal<typeof promiseAllTest2, Promise<[1, 2, number]>>>,
Expect<Equal<typeof promiseAllTest3, Promise<[number, number, number]>>>,
여기서 차이점은 Test2는 as const를 인자로 넣어줬다는 점이고, Test3는 그냥 배열을 넣어줬다는 점이다.
무슨 차이일까?
그것은 바로 컴파일러가 ts파일을 컴파일할 때, 해당 인자를 어디까지 예상하느냐의 차이다.
PromiseAll([1, 2, Promise.resolve(3)] as const)의 경우
[1, 2, Promise.resolve(3)] 자체가 타입이기 때문에 컴파일 시, 배열에 1과 2와 Promise.resolve(3)이 정확하게 들어있다고 파악할 수 있다.
하지만, PromiseAll([1, 2, Promise.resolve(3)])의 경우
컴파일시 1, 2, Promise.resolve(3)을 그저 number라는 타입의 원소가 담긴 배열이라고 인식하기 때문에
[number, number, Promise<number>] 를 리턴하게 된다.
'Language > Typescript' 카테고리의 다른 글
타입챌린지 : 106-Trim Left (medium) (0) | 2023.03.27 |
---|---|
타입챌린지 : 62-Type Lookup (0) | 2023.03.24 |
타입챌린지 : 16-Pop (medium) (0) | 2023.03.22 |
타입챌린지 : 15-Last of Array (medium) (0) | 2023.03.22 |
타입챌린지 : 12-Chainable Options (medium) (0) | 2023.03.21 |