[TS] 조건부 타입

조건부 타입

  • 타입스크립트에서는 조건부 타입을 사용해 조건에 따라 출력 타입을 다르게 도출할 수 있다. 조건부 타입은 자바스크립트의 삼항 연산자와 동일하게 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 가 없는 menuItemname 만 추출하여 유니온 타입을 만들 수 있다.
  • UnpackMenuNames 타입은 불변 객체인 MenuItem 배열만 입력으로 받을 수 있도록 제한되어 있다. 따라서 menuListas 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>; // "기기 내역 관리" | "헬멧 인증 관리" | "운행 관리"

'TypeScript' 카테고리의 다른 글

[TS] NonNullable로 타입 가드  (0) 2025.01.28
[TS] 유틸리티 타입  (0) 2025.01.27
[TS] 식별할 수 있는 유니온, Exhaustiveness Checking  (1) 2025.01.25
[TS] 타입 가드  (1) 2025.01.24
[TS] 타입 확장하기  (1) 2025.01.23