Curt Poem

프론트 엔드 공부와 지식 나눔을 위한 블로그

개발/TypeScript

type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기: Easy 난이도 - 2

Dovelop 2024. 8. 5. 00:03
 

type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기: Easy 난이도 - 1

일반적으로 프로젝트에서 타입스크립트를 사용한다고 해도 복잡한 타입을 만들고 적용할 일은 잘 없었던 것같습니다. 저 또한 함수의 파라미터와 리턴값에서 number, string 등 기본적인 자료형만

curt-poem.tistory.com

 

type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기 시리즈의 2번째 포스팅입니다. 이번 역시 easy난이도입니다. easy난이도는 아마 3번째 시리즈까지 포스팅하면 끝날 것 같습니다. 그럼 바로 시작하겠습니다.

Length of Tuple

요구

배열(튜플)을 받아 길이를 반환하는 제네릭 Length<T>를 구현하세요.

예시

  type tesla = ['tesla', 'model 3', 'model X', 'model Y']
  type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

  type teslaLength = Length<tesla>  // expected 4
  type spaceXLength = Length<spaceX> // expected 5

풀이

type Length<T extends readonly unknown[]> = T['length'];

사실 이 문제는 자바스크립트에서 Array라는 자료구조를 잘 알고 있다면 가장 쉬운 문제가 될 것입니다. 자바스크립트의 Array는 Object로 구현되어 있습니다. 다만 일반 객체와 다른 점은 키가 숫자이고 length라는 속성을 가진다는 것입니다. 배열의 특정 요소에 접근할 때 Array[1]은 해당 객체에 1이라는 키를 가진 키(속성)에 접근하는 것과 같습니다. 따라서 타입스크립트에서도 배열의 길이를 가져올 때 length라는 키에 접근하면 그 값은 배열의 길이가 됩니다.

 

그런데 위의 제넥릭의 결과가 number타입이 아닌 실제 배열의 길이를 상수값으로 가져오는 것에 대해 의문이 생길 것입니다. 왜냐하면 자바스크립트에서 배열의 길이는 동적이며, 실제 런타임에 배열의 길이는 충분히 변경될 수 있기 때문입니다. 그래서 배열의 길이는 숫자의 상수값이 아닌 number가 올바를 것입니다. 맞는 말입니다. Lenth 제네릭이 배열의 길이를 상수로 반환하기 위해서는 해당 배열이 튜플이어야 합니다. 요구에서 배열(튜플)이라고 언급한 부분이 튜플인 배열을 의미합니다, 만약 튜플이 아닌 배열에 Length제네릭을 사용한다면 결과값(타입)은 number가 나오게 됩니다.

const myArray = [1, 2, 3];
type b = Length<typeof myArray>; // type b = number

Exclude

요구

T에서 U에 할당할 수 있는 타입을 제외하는 내장 제네릭 Exclude<T, U>를 이를 사용하지 않고 구현하세요.

예시

type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

풀이

type MyExclude<T, U> = T extends U ? never : T;

이전의 글에서 T extends U ? true : false와 같이 삼항조건 연산자를 타입스크립트에 사용할 수 있다고 하였습니다. 따라서 위의 코드를 해석한다면 TU에 할당 가능한 타입이라면 never가 되고 그렇지 않다면 T타입을 그대로 사용하라는 것입니다.

 

never역시 지난 번 포스팅에서 사용하였지만 지금 다시 살펴보니 별다른 설명을 하지 않고 넘어갔었습니다. 타입스크립트에서 never 타입은 "절대 발생하지 않는 값"을 나타냅니다. 이 타입이 사용된 경우는 코드의 흐름상 논리적으로 불가능한 상황을 나타냅니다. never를 요약하자면 해당 타입은 절대로 발생하지 않는 상황이어서 never 타입을 반환하는 경우 타입 추론과 같이 타입과 관련된 모든 부분에서 제외하라는 의미로 정리 할 수 있습니다. never에 대한 자세한 설명은 곧 작성할 타입스크립트 심층 분석 2편에서 볼 수 있습니다.

 

그럼 다시 코드로 돌아와 코드를 해석한다면 T가 U에 포함되는 경우 T는 결과 타입에서 제외하라는 뜻으로 볼 수 있습니다. 결국 T extends U에서 false 판정을 받은 타입만 살아남게 됩니다.

Awaited

요구

Promise와 같은 타입에 감싸인 타입이 있을 때, 안에 감싸인 타입이 무엇인지 어떻게 알 수 있을까요?
Awaited를 구현해보세요.

예시

Promise<ExampleType>이 있을 때, ExampleType을 어떻게 얻을 수 있을까요?

풀이

type MyAwaited<T> = T extends PromiseLike<infer U> ? MyAwaited<U> : T;

이 문제는 저에겐 많이 어려웠습니다. 이 타입은 T가 PromiseLike 객체인 경우, 그 내부의 값을 계속해서 추출하여 최종적으로는 Promise가 완료된 값을 얻어야 합니다.

 

먼저 T extends PromiseLike<infer U>TPromiseLike 객체인지 확인합니다. PromiseLike는 이름 그대로 Promise와 유사한 객체를 의미합니다. 즉, Promise와 같은 구조를 가지는 인터페이스입니다. 이 인터페이스는 실제 Promise 객체와 유사한 형태를 가지며, 타입스크립트의 타입 시스템에서 비동기 작업을 다룰 때 유용합니다. Promise와 같은 구조를 가지지만, 실제 구현이 아닌 비동기 객체를 나타냅니다.

 

여기서는 infer U를 사용하여 PromiseLike 객체 내부의 값을 추출합니다. 즉 ProimseLikethen 메서드가 실행될 때 가지는 인자의 타입을 추출합니다.

 

처음에 잘못 생각한 점은 then 메서드가 반환하는 타입이 MyAwaited의 반환 타입으로 된다고 생각했던 점입니다. 다시 생각해보면 프로미스의 구조는 다음과 같습니다.

const promise = new Promise<number>((resolve, reject) => {
  resolve(42); // 프로미스의 상태를 pending에서 fulfilled로 변경하여 42라는 값으로 완료 처리
});

위의 코드를 본면 resolve의 인자로 들어오는 값이 프로미스가 완료된 후의 값, 즉 Promise로 감싸여있는 값이 되는 것을 쉽게 이해할 수 있습니다. 그리고 resolve는 프로미스가 성공적으로 완료되었음을 나타내고, resolve를 통해 onfulfilled 콜백이 호출됩니다. 그럼 resolve가 전달받은 인자는 그대로 onfulfilled의 인자가 되며 프로미스가 완료됩니다. 따라서 onfulfilled의 인자로 들어오는 값의 타입을 찾아야 하는 것입니다.

If

요구

조건 C, 참일 때 반환하는 타입 T, 거짓일 때 반환하는 타입 F를 받는 타입 If를 구현하세요. Ctrue 또는 false이고, TF는 아무 타입입니다.

풀이

type If<C extends boolean, T, F> = C extends true ? T : F;

사실 이 문제를 가장 먼저 풀었어야 했는데 문제 번호순으로 풀다보니 뒤늦게 풀게 되었습니다. 이전글에서도 설명하였듯이, A extend ? T : F와 같은 방식으로 삼항 조건 연산자를 사용할 수 있습니다. 이는 AT에 할당할 수 있는 타입이라면 T를 반환 아니라면 F를 반환하라는 뜻입니다. 위의 코드를 해석하면 Ctrue인 경우 T, 그렇지 않다면 F를 반환하는 것입니다.