[4장] 타입 확장하기/좁히기
4.1 타입 확장하기
- 타입 확장은 기존 타입을 사용해서 새로운 타입을 정의하는 것을 말한다.
- 기본적으로 타입스크립트에서는 interface와 type 키워드를 사용해서 타입을 정의하고 extends, 교차 타입, 유니온 타입을 사용하여 타입을 확장한다.
타입 확장의 장점
- 타입 확장의 가장 큰 장점은 코드 중복을 줄일 수 있다는 것이다.
interface BaseMenuItem {
itemName: string | null;
itemImageUrl: string | null;
itemDiscountAmount: number;
stock: number | null;
}
inteface BaseCartItem extends BaseMenuItem {
quantity: number;
}
- interface 대신 type을 사용하는 경우
type BaseMenuItem = {
itemName: string | null;
itemImageUrl: string | null;
itemDiscountAmount: number;
stock: number | null;
};
type BaseCartItem = {
quantity: number;
} & BaseMenuItem;
- 이렇게 사용하면 기존 메뉴 요소에 대한 요구사항이 변경되어도 BaseMenuItem만 수정하면 해결할 수 있다.
유니온 타입
- 유니온 타입은 2개 이상의 타입을 조합하여 사용하는 방법이다.
- 유니온 타입으로 선언된 값은 유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 접근할 수 있다.
interface CookingStep {
orderId: string;
price: number;
}
interface DeliveryStep {
orderId: string;
time: number;
distance: string;
}
function getDeliveryDistance(step: CookingStep | DeliveryStep) {
return step.distance;
// Error: Property distance does not exist on type CookingStep | DeliveryStep
// Error: Property distance does not exist on type CookingStep
}
교차 타입
- 기존 타입을 합쳐 필요한 모든 기능을 가진 하나의 타입을 만드는 것이다.
interface CookingStep {
orderId: string;
time: number;
price: number;
}
interface DeliveryStep {
orderId: string;
time: number;
distance: string;
}
type BaedalProgress = CookingStep & DeliveryStep;
- 이렇게 하면 두 타입을 합쳐 모든 속성을 가진 단일 타입이 된다.
- 교차 타입을 사용할 때 타입이 서로 호환되지 않는 경우도 있다.
type IdType = string | number;
type Numeric = number | boolean;
type Universal = IdType & Numeric;
- 이렇게 지정하면 Universal은 두 타입을 모두 만족하는 number가 된다.
extends와 교차 타입
- 유니온 타입
(|)
과 교차 타입(&)
을 사용한 새로운 타입은 오직 type 키워드로만 선언할 수 있다. - interface를 사용할 경우 extends 키워드를 사용해서 교차 타입을 작성할 수도 있다.
- 주의할 점은 extends 키워드를 사용한 타입이 교차 타입과 100% 상응하지 않는다는 것이다.
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
}
- 위처럼 extends를 사용할 때 이미 상속받은 부분에 대해 새로 선언하면 에러가 발생한다.
- 그러나 type의 &를 사용하면 에러가 발생하지 않고 해당 부분을 never로 인식한다.
4.2 타입 좁히기 - 타입가드
- 타입 좁히기는 변수 또는 표현식의 타입 범위를 더 작은 범위로 좁혀나가는 과정을 말한다.
타입 가드에 따라 분기 처리하기
- 분기 처리는 조건문과 타입 가드를 활용하여 변수나 표현식의 타입 범위를 좁혀 다양한 상황에 따라 다른 동작을 수행하는 것을 말한다.
- 타입 가드는 런타임에 조건문을 사용하여 타입을 검사하고 타입 범위를 좁혀주는 기능을 말한다.
- 타입 가드는 크게 자바스크립트 연산자를 이용한 타입 가드와 사용자 정의 타입 가드로 구분할 수 있다.
- 자바스크립트 연산자를 활용한 타입 가드는 typeof, instanceof, in과 같은 연산자를 사용해서 제어문으로 특정 타입 값을 가질 수밖에 없는 상황을 유도하여 자연스럽게 타입을 좁히는 방식이다.
- 사용자 정의 타입 가드는 사용자가 직접 어떤 타입으로 값을 좁힐지를 직접 지정하는 방식이다.
원시 타입을 추론할 때: typeof 연산자 활용하기
- 자바스크립트의 동작 방식으로 인해 null과 배열 타입 등이 object 타입으로 판별되는 등 복잡한 타입을 검증하기에는 한계가 있다.
- 따라서 typeof 연산자는 주로 원시 타입을 좁히는 용도로만 사용할 것을 권장한다.
const replaceHyphen: (date: string | Date) => string | Date = (date) => {
if (typeof date === string) {
// 이 분기에서는 date의 타입이 string으로 추론된다
return date.replace(/-/g, /);
}
return date;
};
인스턴스화된 객체 타입을 판별할 때: instanceof 연산자 활용하기
- typeof 연산자를 주로 원시 타입을 판별하는 데 사용한다면, instanceof 연산자는 인스턴스화된 객체 타입을 판별하는 타입 가드로 사용할 수 있다.
- A instanceof B 형태로 사용하며 A에는 타입을 검사할 대상 변수, B에는 특정 객체의 생성자가 들어간다.
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 연산자는 객체에 속성이 있는지 확인한 다음에 true 또는 false를 반환한다.
- in 연산자를 사용하면 속성이 있는지 없는지에 따라 객체 타입을 구분할 수 있다.
- in 연산자는 A in B의 형태로 사용하는데 이름 그대로 A라는 속성이 B 객체에 존재하는지를 검사한다.
- 프로토타입 체인으로 접근할 수 있는 속성이면 전부 true를 반환한다.
- in 연산자는 B 객체 내부에 A 속성이 있는지 없는지를 검사하는 것이기 때문에 B 객체에 존재하는 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 형식으로 작성하면 되는데 여기서 A는 매개변수 이름이고 B는 타입이다.
- 참/거짓의 진릿값을 반환하면서 반환 타입을 타입 명제로 지정하게 되면 반환 값이 참일 때 A 매개변수의 타입을 B 타입으로 취급하게 된다.
const isDestinationCode = (x: string): x is DestinationCode => {
return destinationCodeList.includes(x);
};
- 이렇게 사용하면 반환 값이 boolean인 것과 차이가 있다.
- 타입스크립트에게 반환 값에 대한 타입 정보를 알려주고 싶을 때 is를 사용할 수 있다.
- 반환 값의 타입을 x is DestinationCode로 알려줌으로써 타입스크립트는 if문 스코프의 str 타입을 DestinationCode로 추론할 수 있게 된다.
- 객체에
[]
문법으로 키에 접근할 경우 사용할 수 있다.
4.3 타입 좁히기 - 식별할 수 있는 유니온 (Discriminated Unions)
- 종종 태그된 유니온 (Tagged Union) 이라고도 불린다.
- 에러 타입이 3가지가 있고, 아래와 같이 선언되어 있다고 가정하자.
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: '999',
errorMessage: '에러',
toastShowDuration: 3000,
onConfirm: () => {}
}
];
- 위와 같은 상황에서 에러를 발생시키기 위해 식별할 수 있는 유니온을 사용할 수 있다.
- 식별할 수 있는 유니온이란 타입 간의 구조 호환을 막기 위해 타입마다 구분할 수 있는 판별자(discriminant)를 달아주어 포함 관계를 제거하는 것이다.
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;
};
type ErrorFeedbackType = TextError | ToastError | AlertError;
const errorArr: ErrorFeedbackType[] = [
// ...
{
errorType: ALERT,
errorCode: 310,
errorMessage: 얼럿 에러,
toastShowDuration: 5000,
// Object literal may only specify known properties,
// and toastShowDuration does not exist in type AlertError
},
];
- 여기서는 errorType을 추가하여 식별할 수 있는 유니온의 판별자로 사용했다.
- 식별할 수 있는 유니온의 판별자는 유닛 타입unit type으로 선언되어야 정상적으로 동작한다.
- 유닛 타입은 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입을 말한다.
- null, undefined, 리터럴 타입을 비롯해 true, 1 등 정확한 값을 나타내는 타입이 유닛 타입에 해당한다.
- 반면에 다양한 타입을 할당할 수 있는 void, string, number와 같은 타입은 유닛 타입으로 적용되지 않는다.
4.4 Exhaustiveness Checking으로 정확한 타입 분기 유지하기
- Exhaustiveness Checking은 모든 케이스에 대해 철저하게 타입을 검사하는 것을 말하며 타입 좁히기에 사용되는 패러다임 중 하나다.
type ProductPrice = 10000 | 20000 | 5000;
const exhaustiveCheck = (param: never) => {
throw new Error(type error!);
};
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 assign able to parameter of type never
return '배민상품권';
}
};
- exhaustiveCheck라는 함수가 보일 것이다. 이 함수는 매개변수를 never 타입으로 선언하고 있다.
- 즉, 매개변수로 그 어떤 값도 받을 수 없으며 만일 값이 들어온다면 에러를 내뱉는다.
- 이 함수를 타입 처리 조건문의 마지막 else문에 사용하면 앞의 조건문에서 모든 타입에 대한 분기 처리를 강제할 수 있다.