ts 책 스터디(타입 확장하기/좁히기)

MountionRiver·2025년 8월 1일

타입 확장하기

  • 타입확장이란 기존 타입을 사용해서 새로운 타입을 정의하는 것.
  • 기본적으로 타입스크립트는 interface, type 를 사용하여 타입을 정의하고, extends, 교차타입, 유니온 타입을 사용해서 타입을 확장한다.

타입 확장의 장점

  • 타입 확장의 가장 큰 장점은 코드의 중복을 줄일 수 있다는 것. 타입을 새로 만들기 보단 기존 타입을 확장함으로써 불필요한 코드를 줄일 수 있다.

      // 메뉴 요소 타입 interface
    interface BaseMenuItem {
      itemName: string | null;
      itemImageUrl: string | null;
      itemDiscountAmount: number;
      stock: number | null;
    
      // 메뉴 타입에 수량 정보 추가 (extends)
    interface BaseCartItem extends BaseMenuItem {
      quantity: number;
    }
    
      // 메뉴 요소 타입 type
    type BaseMenuItem = {
      itemName: string | null;
      itemImageUrl; string | null;
      itemDiscountAmount: number;
      stock: number/ null;
    };
    
      // 메뉴 타입에 수량 정보 추가
    type BaseCartItem = {
       quantity: number;
    } & BaseMenuItem;

위 예시에서처럼 Base-MenuItem에 있는 속성을 중복해서 작성하지 않고 확장(extends BaseMenuItem)을 활용하여 타입을 정의함으로써 중복된 코드를 줄일 수 있다. 그뿐만 아니라 BaseCartItem이 BaseMenuItem에서 확장되었다는 것을 쉽게 확인할 수 있는 것처럼 더 명시적인 코드를 작성할 수 있게 된다.

타입 확장은 중복 제거, 명시적인 코드 작성 외에도 확장성이란 장점을 가지고 있다. 앞에서 정의한 BaseCartItem을 활용하면 요구 사항이 늘어날 때마다 새로운 CartItem 타입을 확장 하여 정의할 수 있다.

/**
 * 수정할 수 있는 장바구니 요소 타입
 * 품절 여부, 수정할 수 있는 옵션 배열 정보가 추가되었다
 * */
interface EditableCartItem extends BaseCartItem {
  isSoldOut: boolean;
  optionGroups: Selectable0ptionGroup[];
}
/**
 * 이벤트 장바구니 요소 타입
 * 주문 가능 여부에 대한 정보가 추가되었다
 * */
interface EventCartItem extends BaseCartItem {
  orderable: boolean;
}

이 코드에서 BaseCartItem을 확장하여 만든 EditabLeCartItem, EventCartItem 타입을 볼 수 있다. 이렇게 타입 확장을 활용하면 관련된 요구 사항이 생길 때마다 필요 한 타입을 손쉽게 만들 수 있다. 더욱이, 기존 요소에 대한 요구 사항이 변경되어도 BaseCartItem 타입만 수정하고 EditabLeCartItem이나 EventCartItem은 수정하지 않아도 된다.

유니온 타입

  • 2개 이상의 타입을 조합하여 사용하는 방법. 집합적 관점에서 볼때 합집합으로 해석 가능하다.
  type MyUnion = A | B;

유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 접근가능

interface CookingStep {
  orderId: string;
  price: number;
}

interface DeliveryStep {
  orderId: string;
  time: number;
  distance: string;
}

function getDeliveryDistance(step: CookingStep | DeliveryStep) {
  return step.distance
}

getDeliveryDistance 함수는 CookingStep과 DeliveryStep의 유니온 타입 값을 step 이라는 인자로 받고 있지만, 함수 본문에서 step.distance를 호출하고 있는데 distance 는 DeliveryStep에만 존재하는 속성이기 때문에 에러가 발생한다.

교차 타입

  • 기존 타입을 합쳐 필요한 모든 기능을 가진 하나의 타입을 만드는 것
interface CookingStep {
  orderId: string;
  time: number;
  price: number;
}

interface DeliveryStep {
  orderId: string;
  time: number;
  distance: string;
}

type BaedalProgress = CookingStep & DeliveryStep;

여기서 유니온 타입과 다른 점이 있다. BaedaLProgress는 CookingStep과 DeliveryStep 타입을 합쳐 모든 속성을 가진 단일 타입이 된다.

// BaedalProgress 타입의 progress 값은
// CookingStep이 갖고 있는 price 속성과
// Deliverystep이 갖고 있는 distance 속성을 포함하고 있다.

function logBaedalInfo(progress: BaedalProgress) {
  console.log(주문 금액: Sfprogress.price}');
  console.log(배달 거리: Sprogress.distance}');
}
type MyIntersection = A & B;

유니온 타입은 합집합의 개념이고, 교차 타입은 교집합의 개념과 비슷하다.
MyIntersection 타입의 모든 값은 A 타입의 값이며, MyIntersection 타입의 모든 값은 B 타입의 값이다.

/* 배달 팁 */
interface DeliveryTip {
  tip: string;
}
/* 별점 */
interface StarRating {
  rate: number;
}
/* 주문 필터 */
// Filter는 DeliveryTip의 tip 속성과 StarRating의 rate 속성을 모두 만족하는 값이 된다.
type Filter = DeliveryTip & StarRating;

const filter: Filter = {
  tip: 1000원 이하",
  rate: 4,
}

교차 타입을 사용할 때 타입이 서로 호환되지 않는 경우도 있다

// 아래의 Universal 타입은 number 일때만 유효하기 때문에 Universal의 타입은 number이다.
type IdType = string | number;
type Numeric = number | boolean;

type Universal = IdType & Numeric;

extends와 교차타입

  • extends를 사용 하여 교차 타입을 작성 할 수 있다.
interface BaseMenuItem {
  itemName: string | null;
  itemImageUrl: string | null;
  itemDiscountAmount: number;
  stock: number | null;
}

interface BaseCartItem extends BaseMenuItem {
  quantity: number;
}

BaseCartItem은 BaseMenultem을 확장함으로써 BaseMenultem의 속성을 모두 포함하고 있다. 즉, BaseCartItem는 BaseMenultem의 속성을 모두 포함하는 상위 집합이 되고 BaseMenuItem는 BaseCartItem의 부분집합이 된다. 이를 교차 타입의 관점에서 작성하면 다음과 같다.

//유니온 타입과 교차 타입을 사용 한 새로운 타입은 오직 type 키워드로만 선언할 수 있다.
type BaseMenuItem = {
  itemName: string | null;
  itemImageUrl: string | null;
  itemDiscountAmount: number;
  stock: number | null;
}

type BaseCartItem = {
  quantity: number;
} & BaseMenultem;

const baseCartItem: BaseCartItem = {
  itemName: "지은이네 떡볶이",
  itemImageUrl: "https://www.woowahan.com/images/jieun-tteokbokkio.png",
  itemDiscountAmount: 2000,
  stock: 100,
  quantity: 2,
};

extends 키워드를 사용한 타입이 교차 타입과 100% 상응하지는 않는다는 것이다. 아래 예시를 보자.

// interface는 호환되지 않는 타입이 선언되면 에러가 발생한다.
interface DeliveryTip {
  tip: number;
}

interface Filter extends DeliveryTip {
  tip: string;
  // Interface 'Filter' incorrectly extends interface 'DeliveryTip'
  // Types of property 'tip' are incompatible
  // Type 'string' is not assignable to type 'number'
}

// type는 호환되지 않는 타입이 선언되면 never 타입이 된다.
type DeliveryTip = {
  tip: number;
}

type Filter = DeliveryTip & {
  tip: string;

배달의 민족 메뉴 시스템에 타입확장 적용하기(예)

배달의 메뉴 메뉴목록은 이미지와 메뉴명으로 이루어져 있다.

* 메뉴에 대한 타입
* 메뉴 이름과 메뉴 이미지에 대한 정보를 담고 있다 */
interface Menu {
name: string; image: string;
}
function MainMenuO {
// Menu 타입을 원소로 갖는 배열
  const menuList: Menu[] = [{name: "1인분", image: "1인분.png"}, ..]
  return (
    ‹ul>
      {menuList.map((menu) => (
        <li>
          <img src=(menu-image} />
          <span>menu-name/span>
        </li>
      ))}
    </ul>
  )
}

이때 특정 메뉴의 중요도를 다르게 주기 위한 요구 사항이 추가되었다고 가정해보자.

  1. 특정 메뉴를 길게 누르면 gif 파일이 재생되어야 한다.
  2. 특정 메뉴는 이미지 대신 별도의 텍스트만 노출되어야 한다.

요구 사항을 만족하는 타입의 작성 방법을 2가지로 생각해볼 수 있다.
1. 하나의 타입에 여러 속성을 추가하는 방법
2. 타입을 확장하는 방법

/**
 * 방법1 타입 내에서 속성 추가
 * 기존 Menu 인터페이스에 추가된 정보를 전부 추가
 */
interface Menu {
  name: string;
  image: string;
  gif?: string; // 요구 사항 1. 특정 메뉴를 길게 누르면 gif 파일이 재생되어야 한다
  text?: string; // 요구 사항 2. 특정 메뉴는 이미지 대신 별도의 텍스트만 노출되어야 한다
}

/**
 * 방법2 타입 확장 활용
 * 기존 Menu 인터페이스는 유지한 채, 각 요구 사항에 따른 별도 타입을 만들어 확장시키는 구조
 */
interface Menu {
  name: string;
  image: string;
}

/**
 * gif를 활용한 메뉴 타입
 * Menu 인터페이스를 확장해서 반드시 gif 값을 갖도록 만든 타입
 */
interface SpecialMenu extends Menu {
  gif: string; // 요구 사항 1. 특정 메뉴를 길게 누르면 gif 파일이 재생되어야 한다
}

/**
 * 별도의 텍스트를 활용한 메뉴 타입
 * Menu 인터페이스를 확장해서 반드시 text 값을 갖도록 만든 타입
 */
interface PackageMenu extends Menu {
  text: string; // 요구 사항 2. 특정 메뉴는 이미지 대신 별도의 텍스트만 노출되어야 한다
}

다음 처럼 3가지 종류의 메뉴 목록이 있을 때 각 방법을 적용해보자

/**
 * 각 배열은 서버에서 받아온 응답 값이라고 가정
 */
const menulist = [
  { name: "찜", image: "찜.png" },
  { name: "찌개", image: "찌개.png" },
  { name: "회", image: "회.png" },
];

const specialMenuList = [
  { name: "돈까스", image: "돈까스.png", gif: "돈까스.gif" },
  { name : "피자", image: "피자.png", gif: 피자.gif" },
];

const packageMenuList = [
  { name: "1인분", image: 1인분.png", text: 1인 가구 맞춤형" },
  { name:"족발", image: "족발.png", text: "오늘은 족발로 결정" },
]

하나의 타입에 여러 속성을 추가하는 방법

menulist:Menu[]
specialMenuList:Menu[]
packageMenuList:Menu[]

위 방법은 가지고 있지 않은 속성에도 접근이 가능하다 ex)specialMenuList에 text등
이때 에러가 발생한다.

타입을 확장하는 방법

menulist:Menu[]

specialMenuList:Menu[]
specialMenuList:SpecialMenu[]

packageMenuList:Menu[]
packageMenuList:PackageMenu[]

위 방법은 가지고 있지 않은 속성에도 접근이 불가능하기 때문에 타입이 잘못되었음을 미리 알 수 있다.
즉, 무분별하게 속성을 추가하는 것보다 타입을 확장해서 사용하는 것이 좋다.

타입 좁히기 - 타입가드

  • 변수 또는 표현식의 타입 범위를 더 작은 범위로 좁혀나가는 과정. 타입좁히기를 통해 더 정확하고 명시적인 타입 추론을 할 수 있고, 복잡한 타입을 작은 범위로 축소시켜 타입 안정성을 높일 수 있다

타입 가드에 따라 분기 처리하기

  • 타입스크립트에서의 분기 처리는 조건문과 타입 가드를 활용하여 변수나 표현식의 타입범위를 좁혀 다른 상황에 따라 다른 동작을 수행하는 것. 타입 가드는 런타임시에 조건문을 사용해 타입을 검사하고 타입범위를 좁혀준다.
  • 타입정보는 컴파일시 모두 제거되기 때문에 자바스크립트의 typeof, instanceof, in 연산자를 사용하여 타입 좁히기가 가능하다.

원시 타입을 추론할 때 typeof 연산자 활용하기

  • typeof 연산자를 사용할대 typeof A === B 를 조건으로 분기처리 할 수 있다. typeof는 원시타입을 좁히는 용도로만 사용할 것을 권장한다.
const replaceHyphen: (date: string | Date) => string | Date = (date) => {
  if (typeof date === "string") {
    // 이 분기에서는 date의 타입이 string으로 추론된다
    return date.replace(/-/g, "/");
  }

  return date;
}

인스턴스화된 객체 타입을 판별할 때 instanceof 연산자 활용하기

interface Range {
  start: Date;
  end: Date;
}

interface DatePickerProps {
  selectedDates?: Date | Range;
}

const DatePicker = ({ selectedDates }: DatePickerProps) => {
  const [selected, setSelected] = useState(convertToRange(selectedDates));
  //...
};

export function convertToRange(selected?: Date | Range): Range | undefined {
  return selected instanceof Date
    ? {start: selected, end: selected }
    : selected;
}
  • instanceof 연산자는 인스턴스화된 객체 타입을 판별하는 타입 가드로 사용할 수 있다.
  • A instanceof B 형태로 사용하며 A 에는 타입을 검사할 대상 변수, B에는 특정 객체의 생성자가 들어간다.
  • instanceof는 A의 프로토타입 체인에 생성자 B가 존재하는지를 검사해서 존재한다면 true, 그렇지 않다면 false 를 반환한다.
  • 이러한 동작 방식으로 인해 A의 프로토타입 속성 변화에 따라 instanceof 연산자의 결과가 달라질 수 있다는 점은 유의해야 한다.
  • 아래 예시에는 HTMLInputElement에 존재하는 blur 메서드를 사용하기 위해서, event. target이 HTMLInputELement의 인스턴 스인지를 검사한 후 분기 처리하는 로직이 나타나 있다.
const onKeyDown = (event: React.KeyboardEvent) => {
  if (event.target instanceof HTMLInputElement && event.key === "Enter") {
    // 이 분기에서는 event.target의 타입이 HTMLInputELement이며
    // event.key가 'Enter'이다
    event.target.blur();
    onCTAButtonClick(event);
  }
}

객체의 속성이 있는지 없는지에 따른 구분: in 연산자 활용하기

  • in 연산자는 객체에 속성이 있는지 확인한 뒤 boolean 을 반환한다. in 연산자를 사용하면 속성이 있는지 없는지에 따라 객체를 구분할 수 있다.
  • in 연산자는 A in B의 형태로 사용하며 이름 그대로 A라는 속성이 B 객체에 존재하는지를 검사한다. 프로토타입 체인으로 접근할 수 있는 속성이면 전부 true를 반환한다.
  • in 연산자는 B 객체 내부에 A 속성이 있는지 없는지를 검사하는 것이기 때문에 B 객체에 존재하는 A 속성에 undefined를 할당한다고 해서 false를 반환하는 것은 아니다. delete 연산자를 사용하여 객체 내부에서 해당 속성을 제거해야만 false를 반환한다.
interface BasicNoticeDialogProps {
  noticeTitle: string;
  noticeBody: string;
}

interface NoticeDialogWithCookieProps extends BasicNoticeDialogProps {
  cookieKey: string;
  noForADay?: boolean;
  neverAgain?: boolean;
}

export type NoticeDialogProps =
| BasicNoticeDialogProps
| NoticeDialogWithCookieProps;

const NoticeDialog: React.FC‹«NoticeDialogProps>= (props) => { 
  if ("cookieKey" in props) return NoticeDialogWithCookie {...props} />;
  return NoticeDialogBase {..props} />;
};

is 연산자로 사용자 정의 타입 가드 만들어 활용하기

직접 타입 가드 함수를 만들 수도 있다. 이러한 방식의 타입 가드는 반환 타입이 타입 명제인 함수를 정의하여 사용할 수 있다. 타입 명제는 A is B 형식으로 작성할 수 있으며, A는 매개변수 이름이고 B는 타입이다.

  • 참/거짓의 진릿값을 반환하면서 반환 타입을 타입 명제로 지정하게 되면 반환 값이 참일 때 A 매개변수의 타입을 B 타입으로 취급하게 된다.

함수의 반환 값을 boolean이 아닌 x is DestinationCode로 타이핑하여 타입스크립트에게 이 함수가 사용되는 곳의 타입을 추론할 때 해당 조건을 타입 가드로 사용하도록 알려준다. isDestinationCode 함수를 사용하는 예시를 보면서 반환 값의 타입이 boolean인 것과 is를 활용한 것과의 차이를 알아보자.

// string 타입의 매개변수가 destinationCodeList 배열의 원소 중 하나인지를 검사하여 boolean을 반환하는 함수
const isDestinationCode = (x: string): x is DestinationCode =>
 destinationCodeList.includes(x);

const getAvailableDestinationNameList = async 0): Promise‹DestinationNameD> => {
  const data = await AxiosRequest<string[]>("get", "…/destinations");
  const destinationNames: DestinationName[] = [];
  data?.forEach((str) => {
    if (isDestinationCode(str)) {
      destinationNames.push(DestinationNameSet[str]);
      /*
        isDestinationCode의 반환 값에 is를 사용하지 않고 booLean이라고 한다면 다음 에러가 발생한다
        - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record 'MESSAGE_PLATFORM" | "COUPON_PLATFORM" | "BRAZE", "통합메시지플랫폼" | "쿠폰대장간" | "braze">'
      */
    }
  });
  return destinationNames;
}
  • if문 내 isDestinationCode 함수로 data의 str이 destinationCodeList의 문자열 원소 인지 체크하고, 맞는다면 destinationNames 배열에 push한다.
  • 만약 isDestinationcode 의 반환 값 타이핑을 x is DestinationCode가 아닌 boolean으로 했다면 타입스크립트는 어떻게 추론할까? 타입스크립트는 isDestinationCode 함수 내부에 있는 includes 함수를 해석해 타입 추론을 할 수 없기 때문에 타입스크립트는 if문 스코프의 str 타입을 좁히지 못하고 string으로 만 추론한다.
  • destinationNames의 타입은 DestinationName[]이기 때문에 string 타입 의 str을 push할 수 없다는 에러가 발생한다. 이처럼 타입스크립트에게 반환 값에 대한 타입 정보를 알려주고 싶을 때 is를 사용할 수 있다. 반환 값의 타입을 x is DestinationCode로 알려줌으로써 타입스크립트는 if문 스코프의 str 타입을 DestinationCode로 추론할 수 있 게 된다.

타입 좁히기 - 식별할 수 있는 유니온(Discriminated Unions)

에러 정의하기

  • 배달의민족의 선물하기 서비스에선 선물을 보낼 때 필요한 값을 사용자가 올바르게 입력했는지
    를 확인하는 유효성 검사를 진행한다.
  1. 유효성 에러가 발생하면
  2. 사용자에게 다양한 방식으로 에러를 보여 주는데,
  3. 우아한형제들에서는 이 에러를 크게 텍스트 에러, 토스트 에러, 얼럿 에러로 분류한다.
    이들 모두 유효성 에러로 에러 코드erroCode와 에러 메시지errorMessage를 가지고 있으며, 에러 노출 방식에 따라 추가로 필요한 정보가 있을 수 있다. 예를 들어 토스트 에러는 토스트를 얼마 동안 띄울 것인지에 대한 정보가 필요하다.
type TextError = {
  errorCode: string;
  errorMessage: string;
};
type ToastError = {
  errorCode: string;
  errorMessage: string;
  toastShowDuration: number; // 토스트를 띄워줄 시간
};
type AlertError = {
  errorCode: string;
  errorMessage: string;
  onConfirm: () => void; // 얼럿 창의 확인 버튼을 누른 뒤 액션
};

type ErrorFeedbackType = TextError | ToastError | AlertError;
const errorArr: ErrorFeedbackType[] = [
  { errorCode:100, errorMessage: “텍스트 에러” },
  { errorCode:200, errorMessage: “토스트 에러”, toastShowDuration: 3000 },
  { errorCode:300, errorMessage: “얼럿 에러”, onConfirm: () => {} },
];

TextError, ToastError, AlertError의 유니온 타입인 ErrorFeedbackType의 원소를 갖는 배열 errorArr를 정의함으로써 다양한 에러 객체를 관리할 수 있게 되었다. 여기서 해당 배열에 에러 타입별로 정의한 필드를 가지는 에러 객체가 포함되길 원한다고 해보자, 즉. ToastError의 toastShowDuration 필드와 AlertError의 onConfirm 필드를 모두 가지는 객체에 대해서는 타입 에러를 뱉어야 할 것이다.

const errorArr: ErrorFeedbackType[] = [
  // ...
  {
  errorCode:999,
  errorMessage: “잘못된 에러”,
  toastShowDuration: 3000,
  onConfirm: () => {},
  }, // expected error
];

위 코드의 자바스크립트는 덕 타이핑 언어이기 때문에 별도의 타입 에러를 뱉지 않는 것을 확인할 수 있다. 이런 상황에 타입에러가 발생하지 않으면 앞으로의 개발 과정에서 의미를 알 수 없는 무수한 에러 객체가 생겨날 위험성이 커진다.

식별할 수 있는 유니온

  • 식별할 수 있는 유니온이란 타입 간의 구조 호환을 막기 위해 타입마다 구분할 수 있는 판별자를 달아주어 포함 관계를 제거하는 것이다.
  • 식별할 수 있는 유니온을 활용하여 타입들이 서로 포함 관계를 가지지 않도록 정의할 수 있다.

판별자의 개념으로 errorType이라는 필드를 새로 정의해보자. 각 에러 타입마다 이 필드에 대해 다른 값을 가지도록 하여 관별자를 달아주면 이들은 포함 관계를 벗어나게 된다.

type TextError = {
  errorType:TEXT;
  errorCode: string;
  errorMessage: string;
};
type ToastError = {
  errorType:TOAST;
  errorCode: string;
  errorMessage: string;
  toastShowDuration: number;
}
type AlertError = {
  errorType:ALERT;
  errorCode: string;
  errorMessage: string;
  onConfirm: () = > void;
};

에러 객체를 다음과 같이 정의한 상태에서 errorArr을 새로 정의해보자

type ErrorFeedbackType = TextError | ToastError | AlertError;
4
const errorArr: ErrorFeedbackType[] = [
  { errorType:TEXT, errorCode:100, errorMessage: “텍스트 에러” },
  {
    errorType:TOAST,
    errorCode:200,
    errorMessage: “토스트 에러”,
    toastShowDuration: 3000,
  },
  {
    errorType:ALERT,
    errorCode:300,
    errorMessage: “얼럿 에러”,
    onConfirm: () => {},
  },
  {
    errorType:TEXT,
    errorCode:999,
    errorMessage: “잘못된 에러”,
    toastShowDuration: 3000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘TextError’
    onConfirm: () => {},
  },
  {
    errorType:TOAST,
    errorCode:210,
    errorMessage: “토스트 에러”,
    onConfirm: () => {}, // Object literal may only specify known properties, and ‘onConfirm’ does not exist in type ‘ToastError’
  },
  {
    errorType:ALERT,
    errorCode:310,
    errorMessage: “얼럿 에러”,
    toastShowDuration: 5000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘AlertError’
  },
];

위 코드 처럼 작성할경우 정확하지 않은 에러 객쳉 대해 타입에러가 발생하지 않는 것을 볼 수 있다.

식별할 수 있는 유니온 판별자 선정

식별할 수 있는 유니온을 사용할 때 주의할 점이 있다.
식별할 수있는 유니온의 판별자는 유닛 타입unit type으로 선언되어야 정상적으로 동작한다.
유닛 타입은 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입을 말한다.

  • 유닛타입: null, undefined, 리터럴 타입을 비롯해 true, 1 등 정확한 값을 나타내는 타입
  • 유닛타입이 아닌 것: void, string, number와 같은 타입

공식 깃허브의 이슈 탭을 살펴보면 식별할 수 있는 유니온의 판별자로 사용할 수 있는 타입을 다음과 같이 정의하고 있다.

- 리터럴 타입이어야 한다.
- 판별자로 선정한 값에 적어도 하나 이상의 유닛 타입이 포함되어야 하며, 인스턴스화할 수 있는 타입은 포함되지 않아야 한다.
interface A {
  value: “a”; // unit type
  answer: 1;
}

interface B {
  value: string; // not unit type
  answer: 2;
}

interface C {
  value: Error; // instantiable type
  answer: 3;
}

type Unions = A | B | C;
function handle(param: Unions) {
  /** 판별자가 value일 때 */
  param.answer; // 1 | 2 | 3
  // ‘a’가 리터럴 타입이므로 타입이 좁혀진다.
  // 단, 이는 string 타입에 포함되므로 param은 A 또는 B 타입으로 좁혀진다
  if (param.value === “a”) {
    param.answer; // 1 | 2 return;
  }
  // 유닛 타입이 아니거나 인스턴스화할 수 있는 타입일 경우 타입이 좁혀지지 않는다
  if (typeof param.value ===string) {
    param.answer; // 1 | 2 | 3 return;
  }
  if (param.value instanceof Error) {
    param.answer; // 1 | 2 | 3 return;
  }
  /** 판별자가 answer일 때 */
  param.value; // string | Error
  // 판별자가 유닛 타입이므로 타입이 좁혀진다
  if (param.answer === 1) {
    param.value; // ‘a’
  }
}

위 코드에서는 a만이 유일한 유닛 타입이다.

Exhaustiveness Checking으로 정확한 타입 분기 유지하기

  • Exhaustiveness Checking: 모든 케이스에 대해 철저하게 타입을 검사하는 것
  • 타입 가드를 사용해서 타입에 대한 분기 처리를 수행하면 필요하다고 생각되는 부분만 분기 처리를 하여 요구 사항에 맞는 코드를 작성할 수 있게 된다. 하지만 때로는 모든 케이스에 대해 분기 처리를 해야만 유지보수 측면에서 안전하다고 생각되는 상황이 생긴다. 이때 Exhaustiveness Checking을 통해 모든 케이스에 대한 타입 검사를 강제할 수 있다.
type ProductPrice =10000|20000;

const getProductName = (productPrice: ProductPrice): string => {
  if (productPrice ===10000) return “배민상품권 1만 원”;
  if (productPrice ===20000) return “배민상품권 2만 원”;
  else {
    return “배민상품권”;
  }
};

위 코드에서 새로운 상품권이 생겨서 ProductPrice 타입이 업데이트 되어야 한다고 가정해보자.

type ProductPrice =10000|20000|5000;

const getProductName = (productPrice: ProductPrice): string => {
  if (productPrice ===10000) return “배민상품권 1만 원”;
  if (productPrice ===20000) return “배민상품권 2만 원”;
  if (productPrice ===5000) return “배민상품권 5천 원”; // 조건 추가 필요
  else {
    return “배민상품권”;
  }
};

getProductName 함수를 수정하지 않아도 별도 에러가 발생하는 것이
아니기 때문에 실수할 여지가 있다.Exhaustiveness Checking을 사용하면 모든 타입에 대한 타입 검사를 강제 할 수 있다.

type ProductPrice =10000|20000|5000;

const getProductName = (productPrice: ProductPrice): string => {
  if (productPrice ===10000) return “배민상품권 1만 원”;
  if (productPrice ===20000) return “배민상품권 2만 원”;
  // if (productPrice === “5000”) return “배민상품권 5천 원”;
  else {
    exhaustiveCheck(productPrice); // Error: Argument of type ‘string’ is not assignable to parameter of type ‘never’
    return “배민상품권”;
  }
};

const exhaustiveCheck = (param: never) => {
  throw new Error(type error!);
};

위에서 exhaustiveCheck라는 함수가 매개변수를 never 타입으로 선언하고 있다. 즉, 매개변수로 그 어떤 값도 받을 수 없으며 만일 값이 들어온다면 에러를 내뱉는다. 이 함수를 타입 처리 조건문의 마지막 else문에 사용하면 앞의 조건문에서 모든 타입에 대한 분기 처리를 강제할 수 있다.

이렇게 Exhaustiveness Checking을 활용하면 예상치 못한 런타임 에러를 방지하거나 요구사항이 변경되었을 때 생길 수 있는 위험성을 줄일 수 있다.

0개의 댓글