우아한 타입 스크립트 with 리액트 #4

세나정·2024년 5월 30일
0

4장 타입 확장하기좁히기

4.1 타입 확장하기

4. extends와 교차 타입

extends 키워드를 사용하면 교차 타입와 유사한 역할을 수행

유니온 타입과 교차타입을 사용한 새로운 타입 오직 type 키워드로만 선언이 가능

하지만 extends 를 사용한 타입이 교차타입과 100% 상응하진 않음

interface DeliveryTip {
  tip: number;
}

// tip 속성에 대해 정상적으로 에러를 일으키는 경우
// 🚨 Interface 'Filter' incorrectly extends interface 'DeliveryTip'.
// 🚨 Types of property 'tip' are incompatible.
// 🚨 Type 'string' is not assignable to type 'number'.
interface Filter extends DeliveryTip {
  tip: string;
}

// 교차 타입을 통해 만든 경우
type DeliveryTip = {
  tip: number;
}

// ✅ OK
type Filter = DeliveryTip & {tip: string}; // 단 tip속성은 never 타입이 된다.

이 경우 tip 속성의 타입은 ‘never’

type 키워드는 교차 타입으로 선언됐을 때 새롭게 추가되는 속성에 대해 미리 알 수 없기 때문에 선언시엔 에러가 발생하지 않지만, tip이라는 같은 속성에 대해 서로 호환되지 않는 타입이 되어 결국 never가 됨

5. 타입 확장 적용해보기

두 가지 방법이 존재한다고 함. 과연 뭐가 더 나을까

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

/**
 * 방법 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 파일이 재생되어야 한다.
}

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

결론적으로는, 확장성을 이용하는 게 요구사항에 맞추기 쉽고 에러를 통해 잘못을 미리 감지할 수 있음

4.2 타입 좁히기 - 타입가드

TS에서의 분기처리는 조건문과 타입가드를 활용하여 변수나 표현식의 타입 범위를 좁혀 다양한 상황에서 다른 동작을 수행하는 것을 의미 (타입가드는 런타임에서 조건문을 사용하여 타입을 검사하고 범위를 좁힌다고 이해하면 됨)

자바스크립트 연산자를 활용한 타입가드는 typeof, instanceof, in과 같은 연산자를 사용해서 제어문을 활용하는 것
여기에서 자바스크립트를 사용하는 이유는 런타임에 유효한 타입 가드를 만들기 위해서가 크다 (TS뿐만 아니라 JS에서도 사용할 수 있는 문법이여야함)

- 원시 타입 추론시 : typeof 연산자

typeof는 자바스크립트 타입에만 대응이 가능 하지만 JS의 동작 방식으로 인해 null과 배열 등이 object 타입으로 판별되니까 복잡한 타입을 검증하기에는 한계가 존재 그렇기에 원시 타입만을 사용하도록!

- 인스턴스화된 객체 : instanceof 연산자

instanceof는 A의 프로토타입 체인에 생성자 B가 존재하는지를 검사

- 객체에 속성이 있는지 : in 연산자

in 연산자는 B객체 내부에 A속성이 있는지 없는지를 검사하는 것이기 때문에 A 속성이 undefined라고 하더라도 false를 반환하는 것이 아님
delete 연산자를 사용하여 객체 내부 속성을 제거해야 false를 반환

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

- is 연산자로 사용자 정의 타입 가드

직접 타입가드 함수를 만들 수 있음, 반환 타입이 타입 명제 (반환 타입에 대한 타입 가드를 수행하기 위해 사용되는 특별한 형태의 함수)인 함수를 정의하여 사용
A is B라는 식으로 정의

타입 좁히기 로직의 결과를 boolean으로 반환하는데 이때 is 연산자를 통해 반환하는 boolean에 대해 타입의 해석을 입히는 것을 타입 명제라고 함

타입 명제 - 공식 문서

실행 결과가 매개 변수의 타입을 좁혔음을 타입 시스템에 아릴기 위해선 타입 명제를 사용

function isString(value: unknown) value is string {
  return typeof value === 'string';
}

function logValueIfExists(value: string | null | undefined) {
  if(isString(value)) {
 	// ✅ OK
    value.toString();    
  } else {
    console.log(`Value does not exist: ${value}`);
  }
}

4.3 타입 좁히기 - 식별 가능한 유니온

태그된 유니온이라고도 불림

여러가지 타입을 정의 후 유니온 타입을 원소로 하는 배열을 정의했다고 하지만, 따른 에러 객체가 추가되면 자바스크립트는 덕 타이핑 언어이기 때문에 타입 에러를 뱉지 않음

- 식별 할 수 있는 유니온

TS는 구조적 타이핑 특성을 가지고 있기 때문에 조합할 때 타입을 식별할 수 있는 값을 가지지 않으면 타입 시스템이 타입을 구체적으로 특정하지 못하기 때문에 정의하지 않은 속성을 가지고 있는 의미를 알 수 없는 무수한 에러 객체를 만들어도 TS는 문제를 감지 못함

그렇기에 식별할 수 있는 유니온이 나온 것

식별할 수 있는 유니온은 타입 간의 구조 호환을 막기 위해 타입마다 구분할 수 있는 식별자 (discriminant)를 달아주어 포함 관계를 제거하는 것

type TextError = {
  errorType: 'TEXT',
  errorCode: string;
  errorMessage: string;
}

  ...

type ErrorFeedbackType = TextError | ToastError | AlertError;

이렇게 errorType 속성이 판별자로써 추가됨

- 유니온 판별자 선정 방법

판별자는 유닛타입으로 선언되어 있어야함 (다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 갖는 타입)
null, undefined, 리터럴 타입, true, 1 등등 (void, string, number는 해당하지 않음)

공식 깃허브엔 다음처럼 나와 있다고 함

  • 리터럴 타입
  • 판별자로 선정한 값에 하나 이상의 유닛 타입이 포함 되어야 하고 인스턴스화 할 수 있는 타입은 포함되지 않아야함

판별자로 선정한 값에 적어도 하나 이상의 유닛 타입이 포함되어야 하며 이 부분은 판별자가 2개이상의 값을 조합하여 판별자로 사용할 수 있음을 말함

4.4 Exhaustiveness Checking을 통한 타입 분기 유지

가능한 모든 타입 케이스에 대해 철저하게 타입을 검사하는 것 (타입 좁히기에 사용되는 패러다임 중 하나)

타입가드는 분기 처리가 필요하다고 생각되는 부분에만 코드를 작성하지만, 때로는 모든 타입에 분기 처리를 진행해야 안전하다고 생각되는 상황도 존재 (이럴 때 사용하여 모든 케이스에 대한 타입 검사를 강제)

type ProductPrice = '10000' | '20000' | '5000';

const getProductName = (productPrice: ProductPrice): string => {
  if(ProductPrice === '1000') return '배민상품권 1만원';
  if(ProductPrice === '2000') return '배민상품권 2만원';
  // if(ProductPrice === '5000') return '배민상품권 5천원';
  else {
    exhaustiveCheck(productPrice);
    return '배민상품권';
  } 
}

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

이렇게 모든 경우 (else) + 별도의 에러 (exhaustiveCheck)를 통해 실수를 줄일 수 있음

profile
기록, 꺼내 쓸 수 있는 즐거움

0개의 댓글