[TypeScript] infer를 이용해 배열의 구성요소 타입 추출하기
배경
TypeScript로 개발하다 보면 배열을 구성하는 요소의 타입을 추출해야 할 때가 종종 있다. 예를 들어, 서버에서 아래와 같은 응답 타입을 정의했다고 가정해 보자.
interface Response {
posts: {
userId: number;
title: string;
text: string;
}[];
}
이 응답을 받아 리스트 아이템을 렌더링 하는 컴포넌트를 만들 때, 해당 컴포넌트의 Prop 타입에는 보통 posts 배열을 구성하는 요소의 타입인 Post를 정의하고 싶을 것이다. 이때 가장 쉽게 생각할 수 있는 방법 중 하나는 아래와 같이 Post 타입을 별도로 분리하는 것이다.
interface Post {
userId: number;
title: string;
text: string;
}
interface Response {
posts: Post[];
}
// ListItemComponent
interface Props {
post: Post;
}
그러나, 경우에 따라 서버 응답 타입이 백엔드팀에서 제공해 주거나, 외부 라이브러리에 이미 정의되어 있어 Post와 같은 타입을 별도로 분리하기 어려울 수 있다. 이럴 때 배열의 구성요소 타입을 추출할 수 있는 방법을 아라보자.
infer 키워드란?
TypeScript에서 infer 키워드는 조건부 타입(Conditional Types)과 함께 사용되며, 제네릭 타입에서 특정 타입을 추론할 수 있게 해주는 키워드이다. 이 키워드는 타입 시스템 내에서 다양한 타입을 동적으로 추론할 수 있게 해주는 아주 기특한 친구다.
infer를 이용해 배열의 구성요소 타입 추출하기
다음은 배열의 구성요소의 타입을 추출하기 위해 infer 키워드를 사용하는 방법을 보여준다.
type ElementType<T> = T extends (infer U)[] ? U : T;
이 코드를 단계별로 해석해 보자.
- T는 제네릭 타입으로, 이 타입이 배열인지 여부를 extends 키워드를 사용해 확인한다.
- 만약 T가 배열 타입이라면, infer U를 사용하여 배열의 구성요소 타입을 U로 추론한다.
- T가 배열이 아닌 경우에는 그냥 T 자체를 반환한다. 조금 더 엄밀하게 타입을 지정하고 싶다면 unknown과 같은 값을 사용해도 된다.
이 코드를 사용하면, 배열의 구성요소의 타입을 매우 쉽게 추출할 수 있다.
다양한 예제
다양한 예제들을 활용해 ElementType이 어떻게 동작을 하는지 확인해 보자.
type ElementType<T> = T extends (infer U)[] ? U : T;
type StringArray = string[];
type NumberArray = number[];
type UnionArray = (string | number)[];
type ObjectArray = { x: string }[];
type NoneArray = Map<string, string>;
type StringArrayElement = ElementType<StringArray>; // string
type NumberArrayElement = ElementType<NumberArray>; // number
type UnionArrayElement = ElementType<UnionArray>; // string | number
type ObjectArrayElement = ElementType<ObjectArray>; // { x: string }
type NoneArrayElement = ElementType<NoneArray>; // Map<string, string>
위 예제에서 볼 수 있듯이, ElementType은 기본 타입(Primitive Type)뿐만 아니라 유니온 타입(Union Type)과 객체 타입으로 이루어진 배열의 구성요소 타입을 정확하게 추출한다.
결론
TypeScript에서 제공하는 infer 키워드와 조건부 타입을 활용하면 배열의 구성요소 타입을 손쉽게 추출할 수 있다. 이를 활용하면 처음에 고민하던 것을 다음과 같이 해결할 수 있다.
type ElementType<T> = T extends (infer U)[] ? U : T;
interface Response {
posts: {
userId: number;
title: string;
text: string;
}[];
}
type Post = ElementType<Response["posts"]>; // { userId: number; title: string; text: string; }
// ListItemComponent
interface Props {
post: Post;
}
이 외에도 infer를 활용하면 Promise로 감싸진 타입을 추출하거나, 함수의 파라미터와 반환 타입을 추출하는 등 다양한 상황에서 활용할 수 있다.