조건부 타입
- 타입스크립트에서는 조건부 타입을 사용해 조건에 따라 출력 타입을 다르게 도출할 수 있다. 조건부 타입은 자바스크립트의 삼항 연산자와 동일하게
T extends U ? X : Y
형태를 가진다.
- 조건부 타입을 활용하면 중복되는 타입 코드를 제거하고 상황에 따라 적절한 타입을 얻을 수 있기 때문에 더욱 정확한 타입 추론을 할 수 있게 된다.
interface Bank {
financialCode: string;
companyName: string;
name: string;
fullName: string;
}
interface Card {
financialCode: string;
companyName: string;
name: string;
appCardType?: string;
}
// 일반적으로 extends 키워드를 문자열 리터럴과 사용하지는 않는다.
type PayMethod<T> = T extends 'card' ? Card : Bank;
type CardPayMethodType = PayMethod<'card'>;
type BankPayMethodType = PayMethod<'bank'>;
조건부 타입의 필요성
- getRegisteredList 함수는 타입을 구분해서 넣는 사용자의 의도와는 다르게 정확한 타입을 반환하지 못한다. 인자로 넣는 타입에 알맞은 타입을 반환하고 싶지만, 타입 설정이 유니온으로만 되어있기 때문에 타입스크립트는 알맞은 타입을 추론할 수 없다.
- 인자에 따라 반환되는 타입을 다르게 설정하고 싶다면
extends
를 사용한 조건부 타입을 활용해야 한다. 조건부 타입을 활용하면 정확한 반환 타입을 추론하게 만들 수 있다.
// 결제 수단 기본 타입
interface PayMethodBaseFromRes {
financialCode: string;
name: string;
}
interface Bank extends PayMethodBaseFromRes {
fullName: string;
}
interface Card extends PayMethodBaseFromRes {
appCardType?: string;
}
// 프론트에서 관리하는 결제 수단 관련 데이터로 UI를 구현하는 데 사용되는 타입
interface PayMethodInterface {
companyName: string;
}
type PayMethodInfo<T extends Bank | Card> = T & PayMethodInterface;
type PayMethodType = PayMethodInfo<Card> | PayMethodInfo<Bank>;
const getRegisteredList = (
type: 'card' | 'appcard' | 'bank'
): PayMethodType[] => {
const url = `/${type === 'appcard' ? 'card' : type}`;
const result: PayMethodType[] = [];
return result;
}
const list = getRegisteredList('card'); // PayMethodType[]
조건부 타입 활용
- 제한된 제네릭
T extends 'card' | 'appcard' | 'bank'
을 통해 타입을 제한했다. 따라서 개발자는 잘못된 값을 넘길 수 없어 휴먼 에러를 방지할 수 있다.
- 조건부 타입
PayMethodType<T>
을 사용해서 반환 값을 사용자가 원하는 값으로 구체화할 수 있다. 이에 따라 불필요한 타입 가드, 타입 단언 등을 방지할 수 있다.
type PayMethodType<T extends 'card' | 'appcard' | 'bank'> = T extends
| 'card'
| 'appcard'
? PayMethodInfo<Card>
: PayMethodInfo<Bank>;
const getRegisteredList = <T extends 'card' | 'appcard' | 'bank'>(
type: T
): PayMethodType<T>[] => {
const url = `/${type === 'appcard' ? 'card' : type}`;
const result: PayMethodType<T>[] = [];
return result;
}
const list = getRegisteredList('card'); // PayMethodInfo<Card>[]
infer로 타입 추론
infer
는 “추론하다” 라는 의미를 지니고 있는데 타입스크립트에서도 타입을 추론하는 역할을 한다. extends
를 사용할 때 해당 키워드를 사용할 수 있다.
- UnpackPromise 타입은 제네릭으로 T를 받아 T가 Promise로 래핑된 경우라면 K를 반환하고, 아니면 any를 반환한다.
type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;
const promises = [Promise.resolve('Mark' as const), Promise.resolve(38)];
type Expected = UnpackPromise<typeof promises>; // number | "Mark"
subMenus
가 없는 menuItem
의 name
만 추출하여 유니온 타입을 만들 수 있다.
UnpackMenuNames
타입은 불변 객체인 MenuItem
배열만 입력으로 받을 수 있도록 제한되어 있다. 따라서 menuList
도 as const
키워드를 통해 불변 객체로 정의한다. 또한, subMenus
를 가지고 있을 경우 재귀 호출를 수행한다.
interface SubMenu {
name: string;
path: string;
}
interface MainMenu {
name: string;
path?: string;
subMenus?: ReadonlyArray<SubMenu>;
}
type MenuItem = MainMenu | SubMenu;
const menuList = [
{
name: "계정 관리",
subMenus: [
{
name: "기기 내역 관리",
path: "/device-history",
},
{
name: "헬멧 인증 관리",
path: "/helmet-certification",
},
],
},
{
name: '운행 관리',
path: '/operation',
},
] as const;
// 권한이 필요한 메뉴 이름을 추출한다.
type UnpackMenuNames<T extends ReadonlyArray<MenuItem>> = T extends
ReadonlyArray<infer U>
? U extends MainMenu
? U['subMenus'] extends infer V
? V extends ReadonlyArray<SubMenu>
? UnpackMenuNames<V>
: U['name']
: never
: U extends SubMenu
? U['name']
: never
: never;
type PermissionNames = UnpackMenuNames<typeof menuList>; // "기기 내역 관리" | "헬멧 인증 관리" | "운행 관리"