TypeScriptSTUDY _ 4장 . 타입 확장하기 . 좁히기 [ 4.1 타입 확장하기 ]

zeroha·2024년 12월 2일
0

TypeScriptStudy

목록 보기
10/32
post-thumbnail

4.1 타입 확장하기

타입확장 : 기존타입을 사용 -> 새로운 타입을 정의하는 것

타입정의( type, interface ) -> 타입확장( extends, 교차 타입, 유니온 타입)

.
.
.

1. 타입 확장의 장점

1) 코드 중복을 줄일 수 있다.
(필연적으로 중복되는 타입 생김 -> 타입확장으로 불필요한 코드 중복 줄임.)

/**
* 메뉴 요소 타입
* 메뉴 이름, 이미지, 할인율, 재고 정보를 담고 있다
**/
interface BaseMenuItem {
  itemName: string | null; 
  itemImageUrl: string | null; 
  itemDiscountAmount: number;
  stock: number | null;
}

/**
* 장바구니 요소 타입
* 메뉴 타입에 수량 정보가 추가되었다
**/
interface BaseCartItem extends BaseMenuItem {
  quantity: number;
}

장바구니 요소 : 메뉴요소가 가지는 모든 타입이 필요
BaseCartItem : BaseMenuItem에서 확장되었다는 걸 알 수 있음.

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

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

2) 확장성

/**
* 수정할 수 있는 장바구니 요소 타입
* 품절 여부, 수정할 수 있는 옵션 배열 정보가 추가되었다 * 
**/
interface EditableCartItem extends BaseCartItem {
  isSoldOut: boolean;
  optionGroups: SelectableOptionGroup [];
}

/**
* 이벤트 장바구니 요소 타입
* 주문 가능 여부에 대한 정보가 추가되었다 * 
**/
interface EventCartItem extends BaseCartItem {
  orderable: boolean;

요구사항이 늘어날 때마다 새로운 CartItem타입을 확장하여 정의할 수 있다.

타입을 활용하면 장바구니와 관련된 요구 사항이 생길 때마다 필요한 타입을 손쉽게 만들 수 있다.

BaseCartItem 타입만 수정하고 EditableCartItem이나 EventCartItem은 수정하지 않아도 되기 때문에 효율적이다.


2. 유니온 타입

: 2개 이상의 타입을 조합하여 사용하는 방법

type MyUnion = A | B

: MyUnion은 타입 A와 B의 합집합이다.
모든 원소는 집합 MyUnion의 원소이며, 집합 B의 모든 원소 역시 집합 MyUnion의 원소이다.
-> A타입과 B타입의 모든 값 = MyUnion 타입의 값

주의 ) 유니온 타입으로 선언된 값은 유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에 접근 가능

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

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

function getDeliveryDistance(step:CookingStep | DeliveryStep) {
return step.distance;
// Property 'distance' does not exist on type 'CookingStep | DeliveryStep'
// Property 'distance' does not exist on type 'CookingStep'
}

getDeliveryDistance 함수는 CookingStep과 DeliveryStep의 유니온 타입 값을 step이라는 인자로 받고 있다.

  • step.distance 호출
    -> distance는 DeliveryStep에만 존재하는 속성
    -> 인잘 받는 step의 타입이 CookingStep이면 distance 속성을 찾을 수 없어서 에러가 발생한다.
    -> step이라는 유니온 타입 : DeliveryStep타입에 해당할 뿐, CookingStep이면서 DeliveryStep인 것은 아니다.

3. 교차 타입

: 기존 타입을 합쳐 필요한 모든 기능을 가진 하나의 타입을 만드는 것.

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

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

type BaedalProgress = CookingStep & DeliveryStep;

유니온 타입과 다른 점 ) BedalProgresssms CookingStep과 DeliveryStep타입을 합쳐 모든 속성을 가진 단일 타입이 된다.

function logBaedalInfo(progress: BaedalProgress) {
  console.log(`주문 금액: ${progress.price}`);
  console.Log(`배달 거리: ${progress.distance}`);

-> BedalProgress타입의 progress 값 =
CookingStep이 갖고 있는 price 속성 + DeliveryStep이 갖고 있는 distance 속성

type MyIntersection = A & B;

유니온 타입 = 합집합의 개념
교차 타입 = 교집합의 개념
MyIntersection 타입의 모든 값 = A타입의 값
MyIntersection 타입의 모든 값 = B타입의 값
-> MyIntersection 타입의 모든 원소 = 집합 A의 원소 = 집합 B의 원소

/* 배달 팁 */
interface DeliveryTip {
  tip: string;
}
/* 별점 */
interface StarRating {
  rate: number;
}
/* 주문 필터 */
type Filter = DeliveryTip & StarRating;

const filter: Filter = {
  tip: "1000원 이하",
  rate: 4,
};

Filter의 타입1 : DeliveryTip 속성 + StarRating 속성 (모두 포함) <- never타입 x

-> 교차 타입 Filter는 DeliveryTip의 tip 속성 + StarRating의 rate 속성

  • 교차 타입을 사용할 때 타입이 서로 호환되지 않는 경우
type IdType = string | number;
type Numeric = number | boolean;

type Universal = IdType & Numeric;

Universal = IdType과 Numeric의 교차 타입
-> 두 타입을 모두 만족하는 경우에만 유지됨
=> Universal의 타입은 number


4. extends와 교차 타입

: extends 키워드를 사용해 교차 타입을 작성 가능

interface BaseMenuItem {
  itemName: string | null; 
  itemImageUrl: string | null; 
  itemDiscountAmount: number;
  stock: number | null;
}

interface BaseCartItem extends BaseMenuItem = {
  quantity: number;
} 

: BaseCartItem은 BaseMenuItem을 확장 -> BaseMenuItem의 속성을 모두 포함.

interface BaseMenuItem {
  itemName: string | null; 
  itemImageUrl: string | null; 
  itemDiscountAmount: number;
  stock: number | null;
}

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

const baseCartItem: BaseCartItem = {
  itemName : "지은이네 떡볶이",itemImageUrl: "https://www.woowahan.com/images/jieun-tteokbokkio.png",
  itemDiscountAmount: 2000, 
  stock: 100, 
  quantity: 2,
}

: BaseCartItem ( 단일 타입 ) = quantity라는 새로운 속성 + BaseMenuItem의 모든 속성

  • 여기서 type으로 선언한 이유
    : 유니온 타입과 교차 타입을 사용한 새로운 타입은 오직 type 키워드로만 선언이 가능함.

주의) 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'

DeliveryTip 타입은 number 타입의 tip 속성을 가짐.
DeliveryTip을 extends로 확장한 Filter 타입에 string타입의 속성 tip을 선언 -> tip의 타입에 호환되지 않는 에러 발생.

그러나!!

type DeliveryTip = {
  tip: number;
}

type Filter = DeliveryTip & {
  tip: string;
}

extends -> & : 이렇게 바꾸면 에러 발생 x

type의 키워드는 교차 타입으로 선언 되었을 때 : 새롭게 추가되는 속성에 대해 미리 알 수 없기 때문에 에러가 발생 x

  • 이때 tip의 속성은? : never

하지만 tip이라는 같은 속성에 대해 서로 호환되지 않는 타입이 선언되어 결국 never타입이 된 것.


5. 배달의민족 메뉴 시스템에 타입 확장 적용하기

/**
* 메뉴에 대한 타입
* 메뉴 이름과 메뉴 이미지에 대한 정보를 담고 있다
**/

interface Menu {
  name: string;
  image: string;
}

: Menu 인터페이스를 기반으로 사용자에게 메뉴그림들을 보여줌.

function MainMenu() {
  // Menu 타입을 원소로 갖는 배열
  const menuLIst: Menu[]= [{name: 1인분, image: "1인분•png"}, ....]
  
return (
  <ul>
  	{menuList.map ((menu) => (
    ‹li›
      <img src={menu.image} />
      <span>{menu.name}</span>
    </li>
    ))}
  </ul>
 )
}
  • 특적 메뉴의 중요도를 다르게 주기 위해 요구 사항 추가될 때

: 요구 사항을 만족하는 타입의 작성방법 2가지
1. 특정 메뉴를 길게 누르면 gif 파일이 재생되어야 함.
2. 특정 메뉴는 이밎 대신 별도의 텍스트만 노출되어야 함.

/**
* 방법1. 타입 내에서 속성 추가
* 기존 Menu 인터페이스에 추가된 정보를 전부 추가 
**/
interface Menu {
  name: string; 
  image: string;
  gif?: string; // 요구 사항 1. 특정 메뉴를 길게 누르면 
  text?: string; // 요구 사항 2. 특정 메뉴는 이미지 대신 별도의 텍스트만 노출되어야 한다.
}

/**
* 방법2. 타입 확장 활용
* 기존 Menu 인터페이스는 유지한 채, 각 요구 사항에 따른 별도 타입을 만들어 확장시키는 구조 
**/
interface Menu {
  name: string;
  image: string;
}
/**
*gif를 활용한 메뉴 타입
* Menu 인터페이스를 확장해서 반드시 gif 값을 갖도록 만든 타입
**/
interface SpecialMenu extends Menu {
  gif: string; // 요구 사항 1. 특정 메뉴를 길게 누르면 gif 파일이 재생되어야 한다
}

/**
* 별도의 텍스트를 활용한 메뉴 타입
* Menu 인터페이스를 확장해서 반드시 text 값을 갖도록 만든 타입 **/
interface PackageMenu extends Menu {
  text: string; // 요구 사항 2. 특정 메뉴는 이미지 대신 별도의 텍스트만 노출되어야 한다
}
/**
 * 각 배열은 서버에서 받아온 응답 값이라고 가정 ( 3가지 )
**/
const menuList = [
  { name: "찜", image: "찜.png" }, 
  { name: "찌개", image: "찌가.png" },
  { name: "회", image: "회.png" },
];

const specialMenuList = [
  {name: 돈까스, image: "돈까스•png", gif: "돈까스.gif" },
   { name : "피자", image: "피자.png", gif: "피자.gif" },
];

const packageMenuList = [
  { name : "1인분", image: "1인분•png", text: "1인 가구 맞춤형" }, 
  { name: "족발", image: "족발.png", text: "오늘은 족발로 결정" },
];

방법 1. 하나의 타입에 여러 속성을 추가할 때

각 메뉴 목록은 Menu[ ]로 표현

menuList: Menu[] // 0K 
specialMenuList: Menu[] // 0K 
packageMenuList: Menu[] // 0K
  • specialMenuList 배열의 원소가 각 속성에 접근한다고 했을 때.
pecialMenuList.map (menu) => menu.text); // TypeError: Cannot read properties of undefined

: 에러 발생
왜? specialMenuList 배열의 모든 우너소는 text라는 속성을 가지고 있지 않아서

방법 2. 타입을 확장하는 방식

: 각 배열의 타입을 확장할 타입에 맞게 명확히 규정 가능.

enuList: Menu[] // 0K

specialMenuList: Menu[] // NOT OK 
specialMenuList: SpecialMenu[] // 0K

packageMenuList: Menu[] // NOT OK 
packageMenuList: PackageMenu[] // 0K

specialMenuList 배열의 원소 내 속성에 동일하게 접근한다고 가정,
프로그램을 실행하지 않고도 타입이 잘못되었음을 인지 가능.

specialMenuList.map (menu) => menu.text); // Property 'text' does not exist on type 'specialMenu'

주어진 타입에 무분별하게 속성을 추가하여 사용 X -> 타입을 확장해서 사용하는 걸 지향 ( 버그도 예방 가능 )


도서참조 : 우아한 타입스크립트 with 리액트

profile
하 영

0개의 댓글

관련 채용 정보