[TS] 4장 타입 확장하기 좁히기

naini 🐰·2025년 2월 10일

FrontEnd

목록 보기
14/18

📘 학습 후기
이 장의 주제를 보고 타입을 어떻게 잘 관리할 수 있을지 알 수 있을 것이라 기대를 했다. 어떻게 하면 타입 확장, 축소를 통해서 중복을 줄이고 재사용성이 높은 타입 정의를 할 수 있을까?

우아한 타입 스크립트 with 리액트 학습 내용을 정리했다.

1. 타입 확장하기


[1] 타입 확장의 장점

(1) 코드 중복을 줄이고 명시적인 코드를 작성할 수있다

// 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;

(2) 확장성에 좋다

interface EventCartItem extends BaseCartItem { // 이벤트 장바구니 요소 타입
  orderable: boolean;
}
  • 장바구니와 관련된 요구 사항이 생길 때마다 필요한 타입을 쉽게 만들 수 있다.
  • 기존 장바구니 요소에 대한 요구 사항이 변경되어도 BaseCartItem만 수정하고 EventCartItem은 수정하지 않아도 되어서 효율적이다.

[2] 유니온 타입

유니온 타입은 2개 이상의 타입을 조합하여 사용하는 방법이다.
유니온 타입으로 선언된 값은 유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 접근할 수 있다.

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

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

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

(1) 문제점

distance는 DeliveryStep에만 존재하는 속성이다. 인자로 받는 step의 타입이 CookingStep이라면 distance 속성을 찾을 수 없기 때문에 에러가 발생한다.

(2) 해결방법: 타입 가드

function getDeliveryDistance(step: CookingStep | DeliveryStep) {
  if ("distance" in step) {
    return step.distance;
  }
  return "No distance available";
}

(3) 해결방법: Type Assertion

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

[3] 교차 타입

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 타입을 합쳐 모든 속성을 가진 단일 타입이 된다.

(1) 교차 타입을 사용할 때 타입이 서로 호환되지 않는 경우

[4] extends와 교차 타입

(1) 유니온 타입과 교차 타입을 사용한 새로운 타입은 오직 type 키워드로만 선언할 수 있다

type 키워드 문법을 보면 알 수 있다.

// 2. type
type BaseMenuItem  = {
  itemName: string | null;
  itemImageUrl: string | null;;
  itemDiscountAmount: number;
  stock: number | null;
}

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

(2) extends 키워드를 사용한 타입이 교차 타입과 100% 상응하지 않는다

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 타입이 된다.

2. 타입 좁히기 - 타입 가드


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

[1] 타입 가드에 따라 분기 처리하기?

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

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

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

typeof, instanceof, in과 같은 연산자를 사용해서 제어문으로 특정 타입 값을 가질 수밖에 없는 상황을 유도하여 자연스럽게 타입을 좁히는 방식이다.

(2) 사용자 정의 타입 가드

사용자가 직접 어떤 타입으로 값을 좁힐지를 직접 지정하는 방식이다.

[2] 타입 가드의 종류

(1) 원시 타입을 추론할 때: typeof

const replaceHyphen: (date:string | Date) => string | Date = (date) => {
  if (typeof date === "string") {
    return date.replace(/-/g, "/");
  }
  
  return date;
};

(2) 인스턴스화된 객체 타입을 판별할 때: instanceof

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;
  
}
  • instanceof를 사용하면 selectedDate가 Date인지 Range인지 판별이 가능하다.

(3) 객체의 속성이 있는지 없는지에 따른 구분: in

여러 객체 타입을 유니온 타입으로 가지고 있을 때 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} />;
}
  • cookieKey 속성이 있으면 NoticeDialogWithCookieProps 타입으로 판단하여 NoticeDialogWithCookie 컴포넌트를 랜더링한다.
  • 없으면, BasicNoticeDialogProps 타입으로 판단하여 NoticeDialogBase 컴포넌트를 랜더링한다.

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

3. 타입 좁히기 - 식별할 수 있는 유니온


0개의 댓글