1. extends와 제네릭을 활용한 조건부 타입
T extends U ? X : Y
// 타입 T를 U에 할당할 수 있으면 X 타입
// 타입 T를 U에 할당할 수 없으면 Y 타입
TypeScript
복사
interface Bank {
financialCode: string;
fullName: string; // Card와의 차이
name: string;
fullName: string;
}
interface Card {
financialCode: string;
appCardType?: string; // Bank와의 차이
name: string;
fullName: string;
}
// 제네릭 타입으로 extends를 사용한 조건부 타입
type PayMethod<T> = T extends "card" ? Card : Bank;
// 제네릭 매개변수 = "card" : Card 타입
type CardPayMethodType = PayMethod<"card">;
// 제네릭 매개변수 != "card" : Bank 타입
type BankPayMethodType = PayMethod<"bank">;
TypeScript
복사
extends 는 보통 문자열 리터럴과 함께 사용하진 않지만 예시를 위해 사용
2. 조건부 타입을 사용하지 않았을 때의 문제점
type PayMethodType = PayMethodInfo<Card> | PayMethodInfo<Bank>;
export const useGetRegisteredList = (
type: "card" | "appcard" | "bank"
): UseQueryResult<PayMethodType[]> => {
const url = `baeminpay/codes/${type === "appcard" ? "card" : type}`;
const fetcher = fetcherFactory<PayMethodType[]>({
onSuccess: (res) => {
const usablePocketList =
res?.filter(
(pocket: PocketInfo<Card> | PocketInfo<Bank>) =>
pocket?.useType === "USE"
) ?? [];
return usablePocketList;
},
});
// 반환값의 타입을 명확하기 추론하지 못함
const result = useCommonQuery<PayMethodType[]>(url, undefined, fetcher);
return result;
};
TypeScript
복사
위 코드를 만든 개발자는 useGetRegisteredList가 인자의 타입으로 "card", "appcard", "bank" 중 하나를 받아 해당 타입과 알맞은 타입으로 반환까지 해내기를 원했다.
•
"card", "appcard" => Card
•
"bank" => Bank
때문에 타입 PayMethodType을 Card 또는 Bank 타입의 PatMethodInfo 중 하나로 고정하고 반환값 result에 PayMethodType[] 타입을 명시해주었다. 하지만 Card와 Bank 를 명확히 구분하는 로직이 없다. 이것이 문제가 된다.
사용자가 인자로 "card"를 전달했을 때 함수가 반환하는 타입이 PayMethodInfo<Card>[]였으면 좋겠지만, 타입 설정이 유니온(|)으로만 되어있기 때문에 구체적으로 추론할 수 없다.
즉, useGetRegisteredList는 인자로 넣는 타입에 알맞은 타입을 반환하지 못하는 함수다. 유니온 외 다른 조치가 필요하다.
(3) extends 조건부 타입을 활용하여 개선하기
extends 조건부 타입을 활용하여 하나의 API 함수에서 타입에 따라 정확한 반환 타입을 추론하게 만들 수 있다. 또한 extends를 제네릭의 확장자로 활용해서 "card", "appcard", "bank" 외 다른 값이 인자로 들어오는 경우도 방어한다.
// before
type PayMethodType = PayMethodInfo<Card> | PayMethodInfo<Bank>;
// after
type PayMethodType<T extends "card" | "appcard" | "bank"> = T extends
| "card"
| "appcard"
? Card
: Bank;
// PayMethodType의 제네릭으로 받은 값이 "card" 또는 "appcard"면 PayMethodInfo<Card> 타입을 반환
// PayMethodType의 제네릭으로 받은 값이 이외의 값이면 PayMethodInfo<Bank> 타입을 반환
// +) "card" 앞 | 는 줄바꿈 구분자로 이해하면 편함. 한줄로 명확하게 표현하면 아래와 같음
// T extends ("card" | "appcard") ? Card : Bank;
TypeScript
복사
새롭게 정의한 PayMethodType에 제네릭 값을 넣어주기 위해 useGetRegisteredList 함수 인자의 타입을 넣어준다.
// before
export const useGetRegisteredList = (
type: "card" | "appcard" | "bank"
): UseQueryResult<PayMethodType[]> => {
/* ... */
const result = useCommonQuery<PayMethodType[]>(url, undefined, fetcher);
return result;
};
// after
export const useGetRegisteredList = <T extends "card" | "appcard" | "bank">(
type: T
): UseQueryResult<PayMethodType<T>[]> => {
/* ... */
const result = useCommonQuery<PayMethodType<T>[]>(url, undefined, fetcher);
return result;
};
TypeScript
복사
이렇게 조건부 타입을 활용함으로써
•
인자로 "card" 또는 "appcard"를 받으면 PayMethodInfo<Card>를 반환하고
•
인자로 "bank"를 받으면 PayMethodInfo<Bank>를 반환한다.
이에 따라 불필요한 타입 가드와 불필요한 타입 단언을 하지 않아도 된다.