일반적으로 프로젝트에서 타입스크립트를 사용한다고 해도 복잡한 타입을 만들고 적용할 일은 잘 없었던 것같습니다. 저 또한 함수의 파라미터와 리턴값에서 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[]
에는 요소가 없어도 상관이 없습니다.
참고 자료
TypeScript: Documentation The ReadonlyArray Type, Conditional Types
'개발 > TypeScript' 카테고리의 다른 글
타입스크립트 심층 분석 2 - 타입 추론, 특수 타입, const와 let 키워드 차이(+참조값 및 원시값)에 따른 타입 추론 (0) | 2024.08.12 |
---|---|
type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기: Easy 난이도 - 2 (0) | 2024.08.05 |
타입스크립트 심층 분석 1 - 타입스크립트의 역할 및 타입스크립트의 뿌리, 타입(Type) 이해하기 (0) | 2024.08.04 |
타입 추론부터 타입 어설션까지 알아보기(부제: Array.prototype.pop의 반환값에는 항상 undefined가 포함된다?!) (0) | 2024.07.03 |
개발 노트: React navigation에서 덜 귀찮기 위해 더 강력한 타입 사용하기 (0) | 2024.03.27 |