📘 학습 후기
이 장의 주제를 보고 타입을 어떻게 잘 관리할 수 있을지 알 수 있을 것이라 기대를 했다. 어떻게 하면 타입 확장, 축소를 통해서 중복을 줄이고 재사용성이 높은 타입 정의를 할 수 있을까?
우아한 타입 스크립트 with 리액트 학습 내용을 정리했다.
// 1. interface
interface BaseMenuItem { // 메뉴 요소 타입
itemName: string | null;
itemImageUrl: string | null;;
itemDiscountAmount: number;
stock: number | null;
}
interface BaseCartItem extends BaseMenuItem { // 장바구니 요소 타입
quantity: number;
}
// 2. type
type BaseMenuItem = {
itemName: string | null;
itemImageUrl: string | null;;
itemDiscountAmount: number;
stock: number | null;
}
type BaseCartItem = {
quantity: number;
} & BaseMenuItem;
interface EventCartItem extends BaseCartItem { // 이벤트 장바구니 요소 타입
orderable: boolean;
}
유니온 타입은 2개 이상의 타입을 조합하여 사용하는 방법이다.
유니온 타입으로 선언된 값은 유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 접근할 수 있다.
interface CookingStep {
orderId: string;
price: number;
}
interface DeliveryStep {
orderId: string;
time: number;
distance: string;
}
function getDeliveryDistance(step: CookingStep | DeliveryStep) {
return step.distance; // Error
}
distance는 DeliveryStep에만 존재하는 속성이다. 인자로 받는 step의 타입이 CookingStep이라면 distance 속성을 찾을 수 없기 때문에 에러가 발생한다.
function getDeliveryDistance(step: CookingStep | DeliveryStep) {
if ("distance" in step) {
return step.distance;
}
return "No distance available";
}
function getDeliveryDistance(step: CookingStep | DeliveryStep) {
return (step as DeliveryStep).distance;
}
interface CookingStep {
orderId: string;
price: number;
}
interface DeliveryStep {
orderId: string;
time: number;
distance: string;
}
type BaedalProgress = CookingStep & DeliveryStep;
function logBaedalInfo(progress: BaedalProgress) {
console.log(`주문 금액: ${progress.price}`);
console.log(`배달 거리: ${progress.distance}`);
}
BaedalProgress는 CookingStep과 DeliveryStep 타입을 합쳐 모든 속성을 가진 단일 타입이 된다.
type 키워드 문법을 보면 알 수 있다.
// 2. type
type BaseMenuItem = {
itemName: string | null;
itemImageUrl: string | null;;
itemDiscountAmount: number;
stock: number | null;
}
type BaseCartItem = {
quantity: number;
} & BaseMenuItem;
interface DeliveryTip {
tip: number;
}
interface Filter extends DeliveryTip {
tip: string; // tip 타입이 호환되지 않는다는 에러가 발생한다.
}
type DeliveryTip = {
tip: number;
}
type Filter = DeliveryTip & {
tip: string;
}
에러가 발생하지 않는다. 이때 tip 속성의 타입은 never 이다.
type 키워드는 교차 타입으로 선언되었을 때 새롭게 추가되는 속성에 대해 미리 알 수 없기 때문에 선언 시 에러가 발생하지 않는다.
하지만 tip 이라는 같은 속성에 대해 서로 호환되지 않는 타입이 선언되어 never 타입이 된다.
타입스크립트에서 타입 좁히기는 변수 또는 표현식의 타입 범위를 더 작은 범위로 좁혀나가는 과정을 말한다.
타입 좁히기를 통해 더 정확하고 명시적인 타입 추론을 할 수 있게된다.
복잡한 타입을 작은 범위로 축소하여 타입 안정성을 높일 수 있다.
특정 문맥 안에서 타입스크립트가 해당 변수를 타입A로 추론하도록 유도하면서 런타임에서도 유효한 방법이 필요한데, 이때 타입 가드를 사용하면 된다.
타입 가드는 크게 자바스크립트 연산자를 사용한 타입 가드와 사용자 정의 타입 가드로 구분할 수 있다.
typeof, instanceof, in과 같은 연산자를 사용해서 제어문으로 특정 타입 값을 가질 수밖에 없는 상황을 유도하여 자연스럽게 타입을 좁히는 방식이다.
사용자가 직접 어떤 타입으로 값을 좁힐지를 직접 지정하는 방식이다.
const replaceHyphen: (date:string | Date) => string | Date = (date) => {
if (typeof date === "string") {
return date.replace(/-/g, "/");
}
return date;
};
interface Range {
start: Date;
end: Date;
}
interface DatePickerProps {
selectedDates?: Date | Range; // selectedDates는 Date 타입이거나 Range 타입일 수 있다.
}
const DatePicker = ({selectedDates}: DatePickerProps) => {
const [selected, setSelected] = useState(convertToRange(selectedDates));
}
// 1. selected가 Date라면 { start: selected, end: selected }로 변환하여 Range 타입을 반환한다.
// 2. selected가 Range 객체라면 그대로 반환한다.
export function convertToRange(selected?: Date | Range): Range | undefined {
return selected instanceof Date ? {start: selected, end: selected} : selected;
}
여러 객체 타입을 유니온 타입으로 가지고 있을 때 in 연산자를 사용해서 속성의 유무에 따라 조건 분기를 할 수 있다.
in 연산자는 객체에 속성이 있는지 확인하여 true 또는 false를 반환한다.
in 연산자를 사용하면 속성이 있는지 없는지에 따라 객체 타입을 구분할 수 있다.
기본 개념을 보자
const obj = { name: "naini", age: 20 };
console.log("name" in obj}; // true
console.log("height" in obj); // false
실제로 타입에 어떻게 적용하는지 살펴보자
interface BasicNoticeDialogProps {
noticeTitle: string;
noticeBody: string;
}
interface NoticeDialogWithCookieProps extends BasicNoticeDialogProps {
cookieKey: string;
noForADay?: boolean;
}
export type NoticeDialogProps =
| BasicNoticeDialogProps
| NoticeDialogWithCookieProps;
const NoticeDialog: React.FC<NoticeDialogProps> = (props) => {
if("cookieKey" in props) return <NoticeDialogWithCookie {...props} />;
return <NoticeDialogBase {...props} />;
}