type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기 시리즈의 Easy 난이도 마지막 입니다. 그럼 바로 시작하겠습니다.
Concat
요구
JavaScript의 Array.concat
함수를 타입 시스템에서 구현하세요. 타입은 두 인수를 받고, 인수를 왼쪽부터 concat한 새로운 배열을 반환해야 합니다.
예시
type Result = Concat<[1], [2]> // expected to be [1, 2]
풀이
type Concat<T extends readonly unknown[], U extends readonly any[]> = [...T, ...U]
concat은 복잡하지 않은 방법으로 만들 수 있습니다. 먼저 T와 C는 배열이어야 합니다. 하지만 튜플 또한 받을 수 있어야 합니다. 튜플은 고정된 개수와 각 요소에 대한 고정된 타입을 가집니다. 반면 배열은 동일한 타입의 요소를 가변적으로 포함할 수 있습니다. 따라서 길이와 타입이 이미 정해진 튜플이 배열보다 더 구체적이고 제한적입니다. 즉, 더 좁은 타입입니다. 타입스크립트의 공변성 규칙에 따라 더 좁은 타입을 더 넓은 타입에 할당할 수 있기 때문에, 튜플을 배열에 할당할 수 없지만 배열을 튜플에 할당할 수 있습니다.
[...T, ...U]
는 자바스크립트의 전개 구문과 동일한 역할을 합니다.
Includes
요구
JavaScript의 Array.includes
함수를 타입 시스템에서 구현하세요. 타입은 두 인수를 받고, true
또는 false
를 반환해야 합니다.
예시
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
풀이
type Includes<T extends unknown[], U> = Equal<T["length"], 0> extends true
? false
: Equal<T[0], U> extends true
? true
: T extends [infer _, ...infer Rest]
? Includes<Rest, U>
: false;
Includes의 풀이는 생각보다 복잡합니다. 타입스크립트의 타입 시스템은 정적 분석 단계에서만 작동하기 때문에 배열 각각의 요소를 직접적으로 조회하는 것은 불가능하기 때문입니다. 하지만 배열의 특정 인덱스를 가져오는 것은 가능합니다. 배열의 0번 인덱스와 재귀를 이용하면 요소를 하나씩 확인할 수 있습니다.
재귀를 통해 배열의 가장 첫 번째 요소를 확인합니다. 먼저 Equal<T['length'], 0>
는 두가지 역할을 합니다. 먼저 재귀의 종료 조건이 됩니다. 더 이상 조회할 요소가 없으면 false
를 반환합니다. 하지만 더 중요한 역할은 다음 역할인데요. U
가 undefined
일 때 배열 길이 이상의 요소를 조회하면 무조건 true
가 나오기 때문에 배열의 길이를 확인하여 이런 상황을 방지합니다. 다음으로는 배열의 0번 인덱스와 찾으려는 조건인 U
가 일치한다면 true
를 반환하며 재귀를 끝내고 그렇지 않다면 0번 인덱스의 요소는 버리고 infer
키워드를 활요해 Rest
에 나머지 요소를 담은 뒤 Include
를 다시 실행합니다.
Push, Unshift
요구
Array.push
와 Array.unshift
의 제네릭 버전을 구현하세요.
예시
type Result = Push<[1, 2], '3'> // [1, 2, '3']
type Result = Unshift<[1, 2], 0> // [0, 1, 2]
풀이
type Push<T extends unknown[], U> = [...T, U];
type Unshift<T extends unknown[], U> = [U, ...T];
딱히 설명이 필요하지 않을 정도로 쉬운 제네릭입니다. Push
는 기존 배열을 전개 구문을 통해 분래하여 주고 추가하려는 요소를 마지막에 위치시킵니다. Unshift
는 반대로 추가하려는 요소를 앞에 두고 기존 배열을 전개합니다.
Parameters
요구
내장 제네릭 Parameters<T>
를 이를 사용하지 않고 구현하세요.
예시
const foo = (arg1: string, arg2: number): void => {}
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
풀이
type MyParameters<T extends (...params: never[]) => unknown> = T extends (...params: infer U) => any ? U : never;
이제까지 infer
를 잘 사용했지만 이 문제가 Infer
를 익히기 가장 좋은 문제라는 생각이 드는 문제입니다.
먼저 myParameters
의 제네릭은 함수를 받는 타입 파라미터 T를 받습니다. 이때 함수의 인자는 공변성이 아닌 반공변성을 따르기 때문에 unkown
이 아닌 never[]
가 되어야 합니다. 우측의 extends
는 단순 infer
를 사용하기 위함입니다. 그리고 모든 파라미터들은 infer
를 통해 U
에 담기게 됩니다. 그리고 MyParameters
제네릭은 U
를 반환합니다.
'개발 > TypeScript' 카테고리의 다른 글
타입스크립트에서 타입을 직접 정의할 때 알고 있으면 좋은 제네릭들 (0) | 2024.11.17 |
---|---|
타입스크립트 심층 분석 4 - 함수의 포함 관계는 뭔가 다르다. (공변성과 반공변성) (0) | 2024.09.20 |
타입스크립트 심층 분석 3 - 타입스크립트의 구조적 타이핑과 타입호환성 (0) | 2024.08.21 |
타입스크립트 심층 분석 2 - 타입 추론, 특수 타입, const와 let 키워드 차이(+참조값 및 원시값)에 따른 타입 추론 (0) | 2024.08.12 |
type-challenges 문제 풀이로 타입스크립트 타입 시스템 깊이 파기: Easy 난이도 - 2 (0) | 2024.08.05 |