Curt Poem

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

개발/TypeScript

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

Dovelop 2024. 7. 12. 19:49

일반적으로 프로젝트에서 타입스크립트를 사용한다고 해도 복잡한 타입을 만들고 적용할 일은 잘 없었던 것같습니다. 저 또한 함수의 파라미터와 리턴값에서 number, string 등 기본적인 자료형만이 사용했었습니다. 물론 그대로 타이핑하지는 않았지만 interface를 만들어 json 키-값을 타입으로 만든다거나, 컴포넌트의 props의 타입을 지정해주는 것정도에 그쳤습니다. 이 정도로만 사용하여도 타입스크립트의 장점을 가져올 수 있으며, 충분히 편리한 개발 경험을 가질 수 있습니다.

 

하지만, 라이브러리의 내용을 보거나 혹은 단순 성장의 욕구로 타입스크립트를 더 똑똑하게 사용하고 싶었습니다. 저의 경우 리액트 네이티브 프로젝트에서 네비게이션 기능의 타입을 쉽게 쓰기 위해 타입을 만들어 가면서 타입스크립트의 세계도 참 깊겠구나는 생각이 들었습니다. 하지만 어떻게 그 깊은 세계를 탐험할 수 있을지 막막하던 차에 깃허브의 type-challenges라는 레포지토리를 알게 되었고 타입 스크립트 공부에 큰 도움이 될 것이라 생각해 조금씩 문제를 풀어보았습니다. 그리고 esay 난이도의 문제를 해설해보고자 합니다. easy도 공부할게 많아 다음 난이도는 손도 못대고 있습니다.

Pick

요구

T에서 K 프로퍼티만 선택해 새로운 오브젝트 타입을 만드는 내장 제네릭 Pick<T, K>을 이를 사용하지 않고 구현하세요.

예시

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

  type TodoPreview = MyPick<Todo, 'title' | 'completed'>

  const todo: TodoPreview = {
      title: 'Clean room',
      completed: false,
  }

해결

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

T의 프로퍼티 K를 선택해야하기 때문에 K의 값은 T의 프로퍼티 중 하나여야 합니다(테스트케이스에서 K가 T의 프로퍼티가 아닌 것을 가진다면 오류를 발생시키는 것이 옳은 것으로 요구하였습니다). 그래서 K extends keyof T를 사용합니다. MyPick은 새로운 "오브젝트"를 반환해야하는데 이 오브젝트는 키가 K이고 값이 T의 프로퍼티 K의 값이어야 합니다. 이때 테스트 케이스 즉, 요구사항으로는 K가 유니온 타입이 될 수 있다고 하였으므로, [k in K]를 이용하여 동적으로 키를 가져오고 T[k]로 키에 맞는 값을 찾아 할당하였습니다(기존 자바스크립트와 같은 모양의 문법입니다).

Readonly

요구

T의 모든 프로퍼티를 읽기 전용(재할당 불가)으로 바꾸는 내장 제네릭 Readonly<T>를 이를 사용하지 않고 구현하세요.

예시

interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

해결

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

타입스크립트의 내장 제네릭 Readonly<T>은 제네릭 타입 T의 모든 속성을 읽기 전용으로 만듭니다. 앞의 문제와 거의 동일하게 해결할 수 있습니다. T의 프로퍼티로 다시 객체를 만드는 데 이때 모든 속성에 readonly 키워드를 붙여 하나하나를 읽기 전용으로 만들어 주면 해결됩니다.

Tuple to Object

요구

배열(튜플)을 받아, 각 원소의 값을 key/value로 갖는 오브젝트 타입을 반환하는 타입을 구현하세요.

예시

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

해결

이번에는 타입스크립트 완전 초보자라면 단순 문제를 제외하고도 배울 것이 많은 문제입니다.

먼저, 타입스크립트의 튜플에 대해 간단히 짚고 넘어가겠습니다. 튜플은 수정이 불가능한 고정된 배열이라고 생각하시면 됩니다. 배열을 만들고 as const로 타입 캐스팅을 하면 해당 배열은 튜플이 됩니다. 그리고 튜플의 타입은 일반 배열과 달리 각각의 인덱스에 타입이 정해진 모양입니다. 예를 들어 const tuple = ["curt", "poem", 1, false, {"K" : 32}, ["hi"]] as const의 타입은 readonly ["curt", "poem", 1, false, { readonly K: 32; }, readonly ["hi"]]와 같은 모양이 됩니다.

type TupleToObject<T extends readonly (keyof any)[]> = {
  [t in T[number]]: t;
}

여기서 제네릭으로 받는 T가 왜 readonly 속성을 가져야 할까요? 앞서 말했듯이 TupleToObject는 튜플을 받습니다. 그리고 tuple은 수정이 불가능하기 때문에 readonly 속성을 기본적으로 가지고 있습니다. 즉, const tuple = [1,2,3]의 타입은 readonly [1,2,3]입니다. 그리고 readonly any\[\]any[]보다 더 넓은 타입이기 때문에 (keyof any)[]가 된다면 readonly any[]인 튜플 T는 MyReadonly에 할당할 수 없습니다(The ReadonlyArray Type 참고). 이해가 잘 되지 않으신다면 readonly any[]는 getter만 정의된 any[]보다 getter와 setter 모두가 정의된 readonly any[]가 더 넓은 타입이라고 생각하시면 됩니다.(실제로 이렇게 작동하지는 않습니다).

 

그리고 any[]가 아닌 (keyof any)[]를 사용하였습니다. 해당 튜플의 각 요소를 key-value 쌍으로 만들어야 하는데, 만약 키가 될 수 없는 값들이 튜플 T의 요소라면 객체로 만들어지지 않습니다. 참고로 keof any의 타입은 string | number | symbol인 유니온입니다.

First of Array

요구

배열(튜플) T를 받아 첫 원소의 타입을 반환하는 제네릭 First<T>를 구현하세요.

예시

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3

해결

// 1번 해결법
type First<T extends unknown[]> = T extends [] ? never : T[0];
// 2번 해결법
type First1<T extends unknown[]> = T['length'] extends 0 ? never : T[0];
// 3번 해결법
type First2<T extends unknown[]> = T extends [infer U, ...unknown[]] ? U: never;

세 가지의 해결법을 사용할 수 있지만 모두 같은 방법이라고 보아도 무방합니다. 다만 여기서는 자바스크립트의 삼항 조건 연산자를 타입스크립트에서 사용하는 법을 배울 수 있습니다. A extend <type> ? T : F와 같은 방식으로 삼항 조건 연산자를 사용할 수 있습니다. 이는 A가 T에 할당할 수 있는 타입이라면 T를 반환 아니라면 F를 반환하라는 뜻입니다. 쉽게 이해하기 위해 자바스크립트로 표현하자면, typeof A === typeof <type> ? T : F와 비슷하게 작동한다고 볼 수 있습니다. 따라서 1번과 2번 해결법을 살펴본다면 빈 배열이 아니라면 T[0]를, 빈 배열이라면 never를 반환하라는 뜻입니다.

 

3번 해결법은 조금 특이한 연산자가 들어있습니다. [infer U, ...unkown[]]에서 ...unkown[]타입스크립트에서 제공하는 조건부 타입(Conditional Types)의 일부로, 자바스크립트의 비구조화할당(Destructuring Assignment)의 나머지 연산자(Rest operator)와 비슷합니다. 자바스크립트와 똑같이 infer U에 하나를 할당하고 나머지는 모두 unkown[]에 할당합니다. 그럼 infer는 무엇일까요? infer는 타입스크립트에서 변수에 타입을 할당하기 위해 사용하는 키워드입니다.타입스크립트가 타입 추론을 통해 추론된 타입을 변수에 할당하여 다시 사용하여야 할 때 사용합니다. 자바스크립트로 치자면 const와 비슷한 역할을 한다고 생각할 수 있습니다. 다시 3번 해결법으로 돌아가서 마저 설명하면 U에 배열의 첫 번째 타입을 할당하고 그 값을 그대로 반환합니다. T extends [infer U, ...unknown[]]라는 조건은 T가 배열 타입이고 최소한 하나의 요소가 있어 U에 할당할 수 있는지 확인하는 조건입니다. T 배열 안에 요소가 없다면 해당 조건은 false가 됩니다. 나머지 연산자로 쓰인 ...unknown[]에는 요소가 없어도 상관이 없습니다.

참고 자료

Type Challenges

TypeScript: Documentation The ReadonlyArray Type, Conditional Types