Type Challenge

2021-12-292022-05-14
  • Typescript

Type Challenge

타입스크립트가 익숙하지 않던 차에 네이버 FE 뉴스레터 21년 12월 호에서 타입 챌린지를 소개해주어서 이제서야 조금씩 해보고 있다! 전체적으로 타입스크립트에서 제공하는 유틸 타입을 직접 구현해봄으로써 익히는 문제들이 많은 것 같다. 실제로 유틸 타입들을 처음 써본다 ㅎㅎ;;

🥉 easy

4. Pick

타입스크립트 내장 유틸 Pick을 직접 구현하기.
보통 Pick<Type, Keys>로 쓰는데 T에서 특정 K만을 뽑아 타입으로 쓰는 것이다.

type MyPick<T, K extends keyof T> = {
  [key in K]: T[key];
};

7. Readonly

타입스크립트 내장 유틸 Readonly<T>를 구현한다. 모든 프로퍼티를 읽기 전용으로 바꾼다.

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

11. Tuple To Object

type TupleToObject<T extends readonly string[]> = {
  [K in T[number]]: K;
};

원래 코드는 any[]로부터 확장하는 거였는데 요 케이스를 통과하기 위해 string[]으로 수정했다.

// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>;

14. First of Array

배열(튜플)의 첫 번째 원소 타입을 반환하는 제네릭 구현하기

처음엔 완전 단순하게 0번째 인덱스를 이용했는데,

type First<T extends any[]> = T[0];

요기서 걸렸다.

Expect<Equal<First<[]>, never>>,

그래서 이걸 예외처리라고 불러야 하는 건진 잘 모르겠는데, 다음과 같이 수정했다.

type First<T extends any[]> = T extends [] ? never : T[0];

다른 사람들은 다음과 같이 풀었더라.

//
type First<T extends any[]> = T['length'] extends 0 ? never : T[0];

//
type First<T extends any[]> = T extends [first: infer First, ...rest: any[]]
  ? First
  : never;
type First<T extends unknown[]> = T extends [infer First, ...infer Rest]
  ? First
  : never;
type First<T extends any[]> = T extends [infer F, ...infer L] ? F : never;

//
type First<T extends any[]> = T[number] extends never ? never : T[0];
type First<T extends any[]> = T[0] extends T[number] ? T[0] : never;

📝 Infer 키워드

주로 infer R과 같이 많이 쓰인다. 조건문에 쓰이는 타입 중 하나를 이름 붙여서 삼항연산자의 true 절이나 false절에 사용하기 위한 키워드이다. 그러니까 위의 코드를 예시로 들면, 배열의 첫번째 요소의 타입을 First라고 이름붙여서 삼항연산자의 true / false 절에서 사용하고 있는 것이다.

18. Length of Tuple

배열(튜플)의 길이를 나타내는 타입 구현하기

이번에도 역시 완전 간단하게 다음과 같이 작성했다가, 엣지케이스에서 걸렸다.

type Length<T extends any> = T['length']

// 엣지케이스
  // @ts-expect-error
  Length<5>,
  // @ts-expect-error
  Length<'hello world'>,

배열의 길이를 나타내는 것이 문제였으니, any가 아니라 any[]로 수정했다.

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

43. Exclude

역시 타입스크립트에서 지원하는 유틸 Exclude를 직접 구현하는 문제이다.

Exclude<Type, ExcludedUnion> : Type에서 ExcludedUnion에 할당할 수 있는 타입을 제외한다.

extends가 무슨 의미인지 안다면 (하위 집합 개념) 쉽게 풀 수 있다.

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

189. Awaited 🔥

문제 : “Promise와 같은 타입에 감싸인 타입이 있을 때, 안에 감싸인 타입이 무엇인지 어떻게 알 수 있을까요? 예를 들어 Promise<ExampleType>이 있을 때, ExampleType을 어떻게 얻을 수 있을까요?”

문제의 출처는 Advanced TypeScript Exercises - Question 1 라고 한다.

문제의 원 출처에서 제시하는 답은 다음과 같은데, infer 키워드를 핵심으로 설명한다.

type Transform<A> = A extends Promise<infer Inner> ? Inner : never;
type Result = Transform<Promise<string>>; // Result is string type

따라서 위의 케이스를 참고해 다음과 같이 답을 쓰면.. Promise가 겹쳐있는 엣지케이스를 통과하지 못한다.

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

Promise 안에 Promise가 계속 있어도 문제가 없게끔 재귀 형식으로 다음과 같이 작성해준다. 그리고 Promise가 아닐 경우도 체크해준다.

type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer U>
  ? U extends Promise<unknown>
    ? MyAwaited<U>
    : U
  : never;

898. Includes

type Include <T,U>는 U가 T에 속하는지를 확인하는 것으로, 자바스크립트의 includes 메소드와 동일한 기능을 구현해야 한다.
처음엔 이렇게 구현했는데 아니나 다를까 튜플 안에 타입이 오는 경우의 테스트 케이스를 통과하지 못했다.

type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;

해서 수정해보면 다음과 같다. 앞선 문제에서 만들었던 Equal 타입을 활용한다.

type Includes<T extends readonly any[], U> = T extends [infer F, ...infer R]
  ? Equal<F, U> extends true
    ? true
    : Includes<R, U>
  : false;

3312. Parameters 🔥

내장 제네릭 Parameters<T>구현하기

🥈 medium

2. Get Return Type

내장 제네릭 ReturnType<T> 구현하기.
ReturnType 공식 문서도 그렇고 타입 챌린지 문제도 그렇고 보면 타입으로 함수를 받는다. 함수에서 반환하는 값의 타입을 infer 키워드로 잡으면 구현할 수 있다.

type MyReturnType<T extends (...args: any[]) => unknown> = T extends (
  ...args: any[]
) => infer R
  ? R
  : never;

3. Omit

내장 제네릭 Omit<T, K> 구현하기. Type에서 특정 Key 프로퍼티만 제거해 새로운 오브젝트 타입을 만든다. Exclude를 이용하여 다음과 같이 구현할 수 있다.

type MyOmit<T, K> = {
  [key in Exclude<keyof T, K>]: T[key];
};

8. Readonly 2

K가 주어지지 않으면 T의 모든 프로퍼티를 읽기 전용으로, K가 주어지면 T에서 K 프로퍼티만 읽기 전용으로 설정하는 제네릭을 구현하기.

예시

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

const todo: MyReadonly2<Todo, 'title' | 'description'> = {
  title: 'Hey',
  description: 'foobar',
  completed: false,
};

todo.title = 'Hello'; // Error: cannot reassign a readonly property
todo.description = 'barFoo'; // Error: cannot reassign a readonly property
todo.completed = true; // OK

정답

// K가 T의 프로퍼티 중 하나이고, K가 주어지진 않을 땐 T 프로퍼티 전체를 사용할 것이니까
type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [P in K]: T[P];
} & {
  // K를 제외하고 readonly를 품
  [P in Exclude<keyof T, K>]: T[P];
};

9. Deep Readonly

nested 된 객체까지도 재귀적으로 전부! 모든 프로퍼티를 Readonly로 바꾸는 타입이다. 이 챌린지에서는 Object만을 다루며 array, function 등은 고려하지 않는다고 한다.

일단 1depth까지 Readonly로 만드는 것부터 시작하면 다음과 같다.

type DeepReadonly<T> = {
  readonly [key in keyof T]: T[key];
};

만약 T[key]가 객체이면 그 속은 Readonly가 아니므로 재귀적으로 처리를 해주어야 한다. 결국 T[key]가 객체인지를 확인해야 하는데, 타입스크립트의 object는 지금 확인하고자 하는 리터럴 객체가 아니라서 T[key] extends object 로는 해결할 수 없다. 자바스크립트에서 함수도, 배열도 객체이기 때문이다.

대신 keyof T[key] extends never로 문제를 해결한다. keyof T[key] extends never의 의미는 T[key]의 key가 없는 값이라는 것이므로 리터럴 객체가 아니라는 뜻과 같아 조건으로 활용이 가능하다.

type DeepReadonly<T> = {
  readonly [key in keyof T]: keyof T[key] extends never
    ? T[key]
    : DeepReadonly<T[key]>;
};

10. Tuple to Union

튜플의 각 원소들을 유니온 타입으로 만드는 유틸 타입을 구현하라는 문제이다.

예시

type Arr = ['1', '2', '3'];

type Test = TupleToUnion<Arr>; // expected to be '1' | '2' | '3'

정답

type TupleToUnion<T extends any[]> = T[number];

12. Chainable Options

체이닝은 자바스크립트에서 매우 흔하게 쓰이는데, 타입스크립트에서는 어떻게 할 거냐는 문제이다.

🥇 hard

기타 궁금증

Exclude와 Omit의 차이점

타입스크립트에서 Exclude와 Omit의 차이가 궁금해서 찾아보니 이런 글이 있었다.

// not working
type mappedTypeWithOmit = {
  [k in Omit<something, 'def'>]: string;
};

// working
type mappedTypeWithExclude = {
  [k in Exclude<something, 'def'>]: string;
};

유니온 타입으로 객체를 매핑할 때 특정 프로퍼티만 제외하고 싶은데 Omit을 쓸 때는 에러가 난다는 글이었다. 이 글에서 소개한 문제의 원인은 다음과 같다.

Omit is used on interface or object type and we are trying to use it on union string literal. That’s why the error.

…

Exclude is different, it is used to exclude a union type.

Omit은 Exclude로 구현되어있는데, 인터페이스나 객체 타입에 사용할 수 있는 것이고 (key-value pair)
Exclude는 유니온 타입(프로퍼티)에 사용할 수 있는 거라고 함.
그러니까 Omit을 사용해서 에러를 해결하고 원하는 결과를 얻으려면 아래와 같이 사용해야 한다.

// 이게 아니라
type mappedTypeWithOmit = {
  [k in Omit<something, 'def'>]: string;
};

// 이렇게 써야 한다.
type mappedType = {
  [k in something]: string;
};
type mappedTypeWithOmit = Omit<mappedType, 'def'>;

Never

코드 흐름 상 자연적으로 절대 발생할 수 없는 결과가 있는데, 이를 표현하기 위한 타입이 Never이다. 예를들어, 아무런 값도 리턴하지 않는 함수에게 리턴 타입을 Never로 지정할 수 있다.

아무런 값도 리턴하지 않는 함수라고 하니 굉장히 자주 쓰는 () => void 가 생각난다. 근데 여기서 주의해야할 점은 ‘정말로 아무것도 리턴하지 않는가?’ 이다. 자바스크립트는 명시적으로 리턴하는 값이 없을 경우 암시적으로 undefined를 리턴하기 때문이다.

따라서 Never는 절대로! 영원히! 아무것도 리턴하지 않는 함수를 의미할 때 쓰여 throw를 하는 함수에 쓰인다면 void는 console.log 등만을 수행하는 함수에도 쓸 수 있다는 차이점이 있다. void 는 void를 리턴하지만, never는 아무것도 리턴하지 않기 때문이다.

더 자세한 내용은 https://mariusschulz.com/blog/the-never-type-in-typescript#the-difference-between-never-and-void을 참고한다.

A function that doesn’t explicitly return a value implicitly returns the value undefined in JavaScript. Although we typically say that such a function “doesn’t return anything”, it returns. We usually ignore the return value in these cases. Such a function is inferred to have a void return type in TypeScript.

A function that has a never return type never returns. It doesn’t return undefined, either. The function doesn’t have a normal completion, which means it throws an error or never finishes running at all.

Profile picture

emewjin

Frontend Developer

잘못된 내용 혹은 더 좋은 방법이 있으면 언제든지 알려주세요 XD