타입스크립트 - 제네릭을 사용하여 타입 추출하기


원문 : 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) => {},
    };
}

이제 마르게리타 피자의 토핑을 구매하려고 하면 타입 완성 기능이 동작하는 것을 확인할 수 있다.

image

잘 동작한다!

그러나 PizzaMargherita가 이미 토핑 타입 정보를 포함하는데 purchasePizzaIngredients에서 토핑 타입을 또 넘겨주는 것은 중복이라고 생각되지 않는가?

이미 포함된 토핑 타입을 사용할 수는 없을까?

토핑 추출하기

피자에서 토핑 타입을 "추출(extract)"할 것이므로 조건부 타입에 대해 알아야 한다.

type ExtractToppings<P> = P extends Pizza<infer T> ? T : never;

위의 코드는 PPizza 타입일 때 토핑 타입을 '잡아' 반환한다.

그럼 이제 불필요하게 작성한 토핑 타입을 지우고 타입을 확인해보자.

function purchasePizzaIngredients<P>() {
    return {
        purchaseToppings: (toppings: ExtractToppings<P>) => {},
    };
}

image

놀랍다!

함정

사실 이 글을 작성한 이유는 내가 해결한 문제를 공유하기 위해서였다.

피자에 치즈 타입을 추가해보자.

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) 타입이며 기본 타입을 가지고 있다.

image

어라? toppingsnever 타입이 됐다!

문제는 ExtractToppings타입에 있다.

type ExtractToppings<P> = P extends Pizza<infer T> ? T : never;

코드에 사용된 Pizza<infer T>는 치즈의 타입이 기본값인 BaseCheeses가 된다는 것을 의미한다.

그리고 MargheritaCheeses에서는 BaseCheeses에 있는 feta를 이용할 수 없다. → MargheritaCheesesBaseCheeses를 확장하지 않았다.


PizzaMargheritaPizza<infer T, BaseCheeses>를 확장하지 않는다!

그래서 원하지 않는 never타입이 된다..

수정하기

문제가 무엇인지 알았으니 간단히 수정해보자.

type ExtractToppings<P> = P extends Pizza<infer T, any> ? T : never;

any타입을 주어 받을 수 있는 타입을 더 늘려 주었다. 이렇게 하면, 피자에 어떤 종류의 치즈가 들어있는지 신경 쓰지 않고, 토핑 타입에 대해서만 신경쓰겠다는 의미가 된다.

이제 타입 완성 기능이 이전처럼 잘 동작한다 :)

image

결론

보통 타입스크립트의 이런 고급 기능은 프레임워크나 그 비슷한 것을 만드는 경우에나 사용하게 될 것이다.

실제로 이러한 기능을 사용해야 할 경우를 대비하여 타입스크립트가 어떤 기능을 할 수 있는지 미리 알아놓는다면 도움이 될 것이다.

조정은2020.04.02
Back to list