원문 : https://itnext.io/typescript-extract-unpack-a-type-from-a-generic-baca7af14e51
합성 타입(composite type)에서 한 타입을 가져오기 위한 방법을 생각해본적이 있는가?
type Puppy = Animal<BigEyes, SmallNose, SmallEars>;
...
type PuppyEyes = ExtractEyes<Puppy>;
ExtractEyes
는 무엇이고, 어떻게 동작할까?
이를 설명하기 전에 먼저 알아야할 것이 있다.
타입스크립트 2.8에서 소개된 기능으로 조건부 타입이 있다. 조건부 타입을 사용하면 타입을 더 유동적으로 제어할 수 있다.
T extends U ? X : Y
조건부 타입이 무엇이고, 어떻게 적용되는지 이 글에서 설명하지 않겠다. 공식 문서보다 이 개념을 더 잘 설명해주는 자료들이 많이 나와있다.
필자가 찾은 David Sheldrick가 작성한 글에 조건부 타입에 대해 잘 정리되어 있다.
계속 진행하기 전에 조건부 타입에 대한 글을 읽고 개념을 정리하기를 바란다. 이 글에서 만들어 볼 예제를 이해하는데 도움이 될 것이다.
예제는 Puppy말고 Pizza로 작성하였다. 배고픈 상태로 이 글을 썼다고 생각해주길 바란다.
토핑을 추가할 수 있는 피자를 정의해보자
type MargheritaToppings = {
mushrooms?: boolean;
olives?: boolean;
onion?: boolean;
basil?: boolean;
};
type HawaiianToppings = {
pineapple?: boolean;
};
class Pizza<T> {
addToppings(toppings: T) {}
}
class PizzaMargherita extends Pizza<MargheritaToppings> {}
제네릭 피자( Pizza
) 클래스, 두 개의 토핑 옵션, 마르게리타 피자(PizzaMargherita
)를 미리 정의해놓았다.
다음은, 토핑 타입을 받는 함수를 생성하고, 토핑을 구매하는 함수를 공개 함수로 작성하였다.
function purchasePizzaIngredients<P, T>() {
return {
purchaseToppings: (toppings: T) => {},
};
}
이제 마르게리타 피자의 토핑을 구매하려고 하면 타입 완성 기능이 동작하는 것을 확인할 수 있다.
잘 동작한다!
그러나 PizzaMargherita
가 이미 토핑 타입 정보를 포함하는데 purchasePizzaIngredients
에서 토핑 타입을 또 넘겨주는 것은 중복이라고 생각되지 않는가?
이미 포함된 토핑 타입을 사용할 수는 없을까?
피자에서 토핑 타입을 "추출(extract)"할 것이므로 조건부 타입에 대해 알아야 한다.
type ExtractToppings<P> = P extends Pizza<infer T> ? T : never;
위의 코드는 P
가 Pizza
타입일 때 토핑 타입을 '잡아' 반환한다.
그럼 이제 불필요하게 작성한 토핑 타입을 지우고 타입을 확인해보자.
function purchasePizzaIngredients<P>() {
return {
purchaseToppings: (toppings: ExtractToppings<P>) => {},
};
}
놀랍다!
사실 이 글을 작성한 이유는 내가 해결한 문제를 공유하기 위해서였다.
피자에 치즈 타입을 추가해보자.
type MargheritaToppings = {
mushrooms?: boolean;
olives?: boolean;
onion?: boolean;
basil?: boolean;
};
type MargheritaCheeses = {
mozzarella?: boolean;
parmesan?: boolean;
};
type BasePizzaCheeses = {
feta: boolean;
};
class Pizza<T, C = BasePizzaCheeses> {
addToppings(toppings: T) {}
addCheeses(cheeses: C) {}
}
class PizzaMargherita extends Pizza<MargheritaToppings, MargheritaCheeses> {}
치즈는 선택적(optional) 타입이며 기본 타입을 가지고 있다.
어라? toppings
가 never
타입이 됐다!
문제는 ExtractToppings
타입에 있다.
type ExtractToppings<P> = P extends Pizza<infer T> ? T : never;
코드에 사용된 Pizza<infer T>
는 치즈의 타입이 기본값인 BaseCheeses
가 된다는 것을 의미한다.
그리고 MargheritaCheeses
에서는 BaseCheeses
에 있는 feta
를 이용할 수 없다.
→ MargheritaCheeses
는 BaseCheeses
를 확장하지 않았다.
PizzaMargherita
는 Pizza<infer T, BaseCheeses>
를 확장하지 않는다!
그래서 원하지 않는 never
타입이 된다..
문제가 무엇인지 알았으니 간단히 수정해보자.
type ExtractToppings<P> = P extends Pizza<infer T, any> ? T : never;
any
타입을 주어 받을 수 있는 타입을 더 늘려 주었다. 이렇게 하면, 피자에 어떤 종류의 치즈가 들어있는지 신경 쓰지 않고, 토핑 타입에 대해서만 신경쓰겠다는 의미가 된다.
이제 타입 완성 기능이 이전처럼 잘 동작한다 :)
보통 타입스크립트의 이런 고급 기능은 프레임워크나 그 비슷한 것을 만드는 경우에나 사용하게 될 것이다.
실제로 이러한 기능을 사용해야 할 경우를 대비하여 타입스크립트가 어떤 기능을 할 수 있는지 미리 알아놓는다면 도움이 될 것이다.