타입가드 시리즈 1 - typeof, instanceof, in, 사용자 정의(is)

yesolog·2024년 2월 2일

TypeScript

목록 보기
16/17
post-thumbnail

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

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

타입스크립트에서의 분기 처리는 조건문과 타입 가드를 활용하여 변수나 표현식의 타입 범위를 좁혀 다양한 상황에 따라 다른 동작으로 수행하는 것을 말한다. (*타입 가드 : 런타임에 조건문을 사용하여 타입을 검사하고 타입 범위를 좁혀주는 기능) 다음 대화를 살펴보자.

👶🏻 : 어떤 함수가 A | B 타입의 매개변수를 받을 때, 인자 타입이 A 또는 B일 때를 구분해서 로직 처리하고 싶을 땐 어떻게 해야 할까?
👨🏻‍🦳 : if문을 사용해서 처리하면 될 것 같은데!
👶🏻 : if문을 사용하면 컴파일 시 타입 정보는 모두 제거되어 런타임에 존재하지 않기 때문에 타입을 사용하여 조건을 만들 수는 없어. 컴파일해도 타입 정보가 사라지지 않는 방법을 사용해야 해. (👨🏻‍🦳: 떼잉..)

타입에 따라 분기 처리를 하기 위해서는 특정 문맥 안에서 1) 타입스크립트가 해당 변수를 타입 A로 추론하도록 유도하면서 2) 런타임에서도 유효한 방법이 필요한데, 이때 타입 가드를 사용하면 된다.

타입 가드는 크게 자바스크립트 연산자를 사용한 타입 가드와 사용자 정의 타입 카드로 구분할 수 있다.

자바스크립트 연산자를 활용한 타입 가드

  • typeof, instanceof, in과 같은 연산자를 사용해서 제어문으로 특정 타입 값을 가질 수밖에 없는 상황을 유도하여 자연스럽게 타입을 좁히는 방식
  • 자바스크립트 연산자를 사용하는 이유 : 런타임에 유효한 타입 가드를 만들기 위해 (런타임에 유효하다 == TS뿐만 아니라 JS에서도 사용할 수 있는 문법이어야 한다.)
  • 활용 예시 : 원시 타입을 추론할 때(typeof 연산자), 인스턴스화된 객체 타입을 판별할 때(instanceof 연산자), 객체의 속성이 있는지 없는지에 따른 구분(in 연산자)

사용자 정의 타입 가드

  • 사용자가 직접 어떤 타입으로 값을 좁힐지를 직접 지정하는 방식
  • 활용 예시 : 타입 명제인 함수를 정의하여 사용(is 연산자)

🟢 2. 원시 타입을 추론할 때: typeof 연산자 활용

typeof A === B를 조건으로 분기 처리하면, 해당 분기 내에서는 A의 타입이 B로 추론된다. 다만 typeof는 자바스크립트 타입 시스템만 대응할 수 있다. 자바스크립트의 동작 방식으로 인해 null과 배열 타입 등이 object 타입으로 판별되는 등 복잡한 타입을 검증하기에는 한계가 있기 때문에 주로 원시 타입을 좁히는 용도로만 사용할 것을 권장한다.

typeof 연산자를 사용하여 검사할 수 있는 타입 목록

  • string
  • number
  • boolean
  • undefined
  • object
  • function
  • bigint
  • symbol
const replaceHyphen: (date: string | Date) => string | Date = (date) => {
  if (typeof date ===string) {
  // 이 분기에서는 date의 타입이 string으로 추론된다
  return date.replace(/-/g,/);
  }

  return date;
};

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

instanceof 연산자는 인스턴스화된 객체 타입을 판별하는 타입 가드로 사용할 수 있다.
A instanceof B 형태로 사용하며 A에는 타입을 검사할 대상 변수, B에는 특정 객체의 생성자가 들어간다.
instanceof는 A의 프로토타입 체인에 생성자 B가 존재하는지를 검사해서 존재한다면 true, 그렇지 않다면 false를 반환한다.

이러한 동작 방식으로 인해 A의 프로토타입 속성 변화에 따라 instanceof 연산자의 결과가 달라질 수 있다는 점은 유의해야 한다.

const onKeyDown = (event: React.KeyboardEvent) => {
  if (event.target instanceof HTMLInputElement && event.key === “Enter”) {
  // 이 분기에서는 event.target의 타입이 HTMLInputElement이며
  // event.key가 ‘Enter’이다
  event.target.blur();
  onCTAButtonClick(event);
  }
};

HTMLInputElement에 존재하는 blur 메서드를 사용하기 위해, event.target이 HTMLInputElement의 인스턴스인지를 검사한 후 분기 처리하는 로직이다.


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

in 연산자는 객체에 속성이 있는지 확인한 다음에 true 또는 false를 반환한다.
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} />; // NoticeDialogWithCookieProps 타입
  return <NoticeDialogBase {...props} />; // BasicNoticeDialogProps 타입
};

NoticeDialogWithCookieProps는 BasicNoticeDialogProps를 상속받고 cookieKey 속성을 가진다. 따라서 두 객체 타입을 cookieKey 속성을 가졌는지 아닌지에 따라 in 연산자로 조건을 만들 수 있다.

자바스크립트의 in 연산자는 런타임의 값만을 검사하지만 타입스크립트에서는 객체 타입에 속성이 존재하는지를 검사한다. 위의 코드처럼 여러 객체 타입을 유니온 타입으로 가지고 있을 때 in 연산자를 사용해서 속성의 유무에 따라 조건 분기를 할 수 있다.


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

사용자 정의 타입 가드는 반환 타입이 타입 명제(type predicates)인 함수를 정의하여 사용할 수 있다. 타입 명제는 A is B 형식으로 작성하면 되는데 여기서 A는 매개변수 이름이고 B는 타입이다. 참/거짓의 진릿값을 반환하면서 반환타입을 타입 명제로 지정하게 되면 반환 값이 참일 때 A 매개변수의 타입을 B 타입으로 취급하게 된다.

*타입 명제(type predicates) : 함수의 반환 타입에 대한 타입 가드를 수행하기 위해 사용되는 특별한 형태의 함수이다.

const isDestinationCode = (x: string): x is DestinationCode =>
  destinationCodeList.includes(x);

isDestinationCode는 string 타입의 매개변수가 destinationCodeList 배열의 원소 중 하나인지를 검사하여 boolean을 반환하는 함수이다. 함수의 반환값을 booelan이 아닌 x is DestinationCode로 타이핑하여 타입스크립트에게 이 함수가 사용되는 곳의 타입을 추론할 때 해당 조건을 타입 가드로 사용하도록 알려준다.

isDestinationCode의 반환 값 타이핑을 x is DestinationCode가 아닌 boolean으로 했을 때

const isDestinationCode = (x: string): boolean =>
  destinationCodeList.includes(x);
const getAvailableDestinationNameList = async (): Promise<DestinationName[]> => {
  const data = await AxiosRequest<string[]>(get,.../destinations”);
  const destinationNames: DestinationName[] = [];
  data?.forEach((str) => {
  if (isDestinationCode(str)) { // str이 destinationCodeList의 원소가 맞는지 체크.
    destinationNames.push(DestinationNameSet[str]); // 맞다면 DestinationNames 배열에 push.
    /*
    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”,  // string[] 타입인 str을 DestinationName[]에 push할 수 없다는 에러
    “통합메시지플랫폼” | “쿠폰대장간” | “braze”>’
    */
    }
  });
  return destinationNames;
};

이 경우 타입스크립트는 isDestinationCode 함수 내부에 있는 includes 함수를 해석해 타입 추론을 할수 없다. 타입스크립트는 if문 스코프의 str 타입을 좁히지 못하고 string으로만 추론한다. destinationNames의 타입은 DestinationName[]이기 때문에 string 타입의 str을 push할 수 없다는 에러가 발생한다.

➡️ 이처럼 타입스크립트에게 반환 값에 대한 타입 정보를 알려주고 싶을 때 is를 사용할 수 있다. 반환 값의 타입을 x is DestinationCode로 알려줌으로써 타입스크립트는 if문 스코프의 str 타입을 DestinationCode로 추론할 수 있다.


출처
도서: 우아한 타입스크립트 with 리액트
자료: 우아한 타입스크립트 스터디
이미지: Freepik

0개의 댓글