Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Tags more
Archives
Today
Total
관리 메뉴

개발의변화

우아한타입스크립트 4장 본문

카테고리 없음

우아한타입스크립트 4장

refindmySapporo 2023. 11. 9. 19:02
반응형

타입 확장이란?

기존 타입을 사용해서 새로운 타입을 정의

타입스크립트 타입 지정 동작원리

- 타입 정의
ex)interface, type

- 타입 확장
ex)extends, 교차타입, 유니온타입

그러면 타입 확장의 장점은?

코드 중복 줄이기!

코드를 작성할 때 불필요한 중복 타입을 선언하기 보단 기존에 작성한 타입 바탕으로 확장

EX)Interface로 지정할 때

interface BaseMenuItem {
 itemName: string | null;
 itemImageUrl: string | null;
}

interface BaseCartItem extends BaseMenuItem {
 quantity: number;
}

BaseCartItem에 BaseMenuItem 확장하여 사용

EX) type으로 지정할 때

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

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

유니온 타입

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

Point) 유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 오직 접근가능

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

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

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

교차타입

기존타입 합쳐 필요한 모든 기능을 가진 하나의 타입

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

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

function checkTime(step: CookingStep & DeliveryStep) {
 return step.distance
}

에러가 안뜨는 이유: 교차 타입은 두 타입의 교집합을 의미 즉 DeliveryTip과 StarRating의 속성을 모두 포함한 타입

하지만 교차 타입을 사용할 때 서로 호환되지 않는 경우동 있다

type IdType = string|number;
type Numeric = number | boolean
type Universal = IdType & Numberic

Universal은 IdType과 Numberic의 교차타입이므로 두 타입 모두 만족하는 경우에만 유지 number!

extends와 교차타입

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

interface BaseCartItem extends BaseMenuItem {
 quantity: number;
}

BaseCartItem은 BaseMenuItem을 확장함으로써 BaseMenuItem의 속성을 모두 포함.

하지만 유니온과 교차타입은 type애서만 사용가능함

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

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

교차 타입과 비슷해 보일 수 있찌만 extends는 100%상응하지 않는다는 것

interface DeliveryTip {
 tip: number;
}

interface Filter extends DeliveryTip{
 tip: string;
}

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

interface Menu {
 name: string;
 image: string;
}
function MainMenu(){
 const menuList: Menu[] = [{name: "1인분", image: "1인분.png"}]

 return(
   <ul>
   {
   manuList.map((menu) => (
    <li>
   <img src = {menu.image} />
   <span> {menu.name} </span>
    </li>
 )) 
   }

   </ul>

 )

}

요구사항
1.특정메뉴를 길게 누르면 gif파일 재생
2.특정 메뉴는 이미지 대신 별도의 텍스트만 노출

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

interface Menu {
 name: string;
 image: string;
 gif?: string;
 text?: string;
}

menu 인터페이스 하나에 모든걸 설정했을 때

specialMenuList.map((menu) => menu.text);
//TypeError: Cannot read properties of undefined

specialMenuList는 Menu 타입의 원소를 가지지만 text 속성에도 접근가능하지만 모든 원소가 text를 안가지기에 에러 발생

2.extends를 활용해 나타낼 떄

interface SpecialMenu extends Menu {
 gif: string;
}
specialMenuList.map((menu) => menu.text); // property 'text' does not exist on type

프로그램을 실행하지 않고도 타입이 잘못되었다는 걸 바로 확인가능

타입좁히기 - 타입가드

변수 또는 표현식의 타입 범위를 더 작은 범위로 좁혀나가는 과정
타입 좁히기 -> 더 정확하고 명시적인 타입 추론을 할 수 있겟되고 복잡한 타입을 작은 범위로 축소하여 타입 안정성 높임

타입 가드: 런타임에 조건물을 사용하여 타입을 검사하고 타입 범위를 좁혀주는 기능

EX)
A | B의 매개변수를 어떤 함수가 받을 때 인자 타입을 A와 B를 구분해서 로직을 처리하고 싶을 때

[X]: if문으로는 컴파일시 타입 정보가 사라져 조건을 만들 수 없다

결국, 특정 문맥 안에서 타입스크립트가 해당 변수를 타입 A로 추론하면서 유도하면서 런타임에서도 유효한 방법인 타입 카드를 사용해야 한다.

방식1: 자바스크립트 연산자를 활용한 타입카드 (typeof, instanceof,in 연산자 활용)

1) typeof - 원시 타입 추론
원시형: string, number, boolean, undefined, object, function, bigint, symbol
typeof A === B 조건으로 분기처리하여 A의 타입을 B로 추론
한계 null, 배열,object 타입을 확인할 수 없다 -> typeof는 자바스크립트 타입 시스템이기 때문

const replaceHyphen: (date: string | Date) => string | Date = (date) => {
  if (typeof date === “string”) {
  // 이 분기에서는 date의 타입이 string으로 추론된다
  return date.replace(/-/g, “/”);
  }

  return date;
};

2) instanceof - 인스턴스화된 객체 타입 판별

interface Range {
 start: Date;
 end : Date;
}

interface DatePickerProps {
 selectedDates?: Date: Range;
}

const DatePicker = ({ selectedDates }: DatePickerProps) => {
 const [selected,setSelected] = useState(convertToRange(selectedDates));

}

export function converToRange(selected?: Date | Range): Range| undefiend {
 return selected instanceof Date
 ? { start: selected, end: selected}
 : selected;
}
타입을 검사할 대상 변수 instanceof 특정객체 생성자

A의 프로토타입 체인에 생성자 B가 존재하는지를 검사하여 존재하면 true, 아니면 false

3) in 연산자 - 객체의 속성이 있는지 없는지 구분

interface BasicNoticeDialogProps {
 noticeTitle: string;
 noticeBody: string;
}

interface NoticeDialogWithCookieProps extends BasicNoticeDialogProps {
 cookieKey: string;
 noForADay?: boolean;
 neverAgain?: boolean
}

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

자바스크립트의 in 연산자는 런타임의 값만을 검사하지만 타입스크립트에서 객체 타입에 속성이 존재하는지 검사

방식2: 사용자 정의 타입 가드는 사용자가 직접 어떤 타입으로 값을 좁힐지 지정하는 방법

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

'매개변수is타입' 형식으로 타입명제를 작성하면 된다.

const isDestinationCode = (x: string): x is DestinationCode => destinationCodeList.includes(x);

함수의 반환 값을 boolean이 아닌 x is D

그러면 boolean을 쓰면 안되나???

const getAvailableDestinationNameList = async (): Promise<DestinationName[]> => {
  const data = await AxiosRequest<string[]>("ㅎㄷㅅ",".../destinations");
  const destinationNames : DestinationName[] = [];
  data?.forEach((str) => {
  if (isDestinationCode(str)) {
   destinationNames.push(DestinationNameSet[str]);
  }

  });
  return destinationNames;
}

isDestinationcode의 x is DestinationCode가 boolean이면 -> 타입 추론 불가능(불린이지만 Destination것인지느 모르기 떄문에)

타입 좁히기 - 식별할 수 있는 유니온(Discriminated Unions)

Ex) 배달의 민족 선물하기 서비스는 선물을 보낼 떄 필요한 값을 사용자가 올바르게 입력햇는지를 확인하는 유효성 검사 진행
유효성에러 = 에러코드 + 에러메시지, 에러 노출 방식에 따라 추가로 필요한 정보 있을 수 있음

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: “100”, errorMessage: “텍스트 에러” },
  { errorCode: “200”, errorMessage: “토스트 에러”, toastShowDuration: 3000 },
  { errorCode: “300”, errorMessage: “얼럿 에러”, onConfirm: () => {} },
];

근데 이제 ToastError의 toastShowDuration 필드와 AlertError의 onConfirm 필드를 가지는 객체에 대해 타입에러를 뱉을까?


const errorArr: ErrorFeedbackType[] = [
  // ...
  {
  errorCode: “999”,
  errorMessage: “잘못된 에러”,
  toastShowDuration: 3000,
  onConfirm: () => {},
  }, 
];

자바스크립트는 덕 타이핑이라 별도의 타입에러를 뱉지 않는다.

식별할 수 있는 유니온

타입들이 서로 포함 관계를 가지지 않도록 정의 , 식별할 수 있는 유니온이란 타입 간의 구조 호환을 막기 위해 판별자를 달아줘 제거

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: “TEXT”, errorCode: “100”, errorMessage: “텍스트 에러” },
  {
    errorType: “TOAST”,
    errorCode: “200”,
    errorMessage: “토스트 에러”,
    toastShowDuration: 3000,
  },
  {
    errorType: “ALERT”,
    errorCode: “300”,
    errorMessage: “얼럿 에러”,
    onConfirm: () => {},
  },
  {
    errorType: “TEXT”,
    errorCode: “999”,
    errorMessage: “잘못된 에러”,
    toastShowDuration: 3000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘TextError’
    onConfirm: () => {},
  },
  {
    errorType: “TOAST”,
    errorCode: “210”,
    errorMessage: “토스트 에러”,
    onConfirm: () => {}, // Object literal may only specify known properties, and ‘onConfirm’ does not exist in type ‘ToastError’
  },
  {
    errorType: “ALERT”,
    errorCode: “310”,
    errorMessage: “얼럿 에러”,
    toastShowDuration: 5000, // Object literal may only specify known properties, and ‘toastShowDuration’ does not exist in type ‘AlertError’
  },
];

식별할 수 있는 유니온을 통해 에러 객체에 대한 타입 에러 발생확인 가능

식별할 수 있는 유니온의 판별자 선정

식별할 수 있는 유니온의 판별자는 유닛타입으로 선언되어야 정상적 동작
유닛타입: 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값 (null,undefined,리터럴, ture, 1 등 정확한 값 / void,string,number은 포함x)

interface A {
  value: “a”; // unit type
  answer: 1;
}

interface B {
  value: string; // not unit type
  answer: 2;
}

interface C {
  value: Error; // instantiable type
  answer: 3;
}

type Unions = A | B | C;
function handle(param: Unions) {
  /** 판별자가 value일 때 */
  param.answer; // 1 | 2 | 3
  // ‘a’가 리터럴 타입이므로 타입이 좁혀진다.
  // 단, 이는 string 타입에 포함되므로 param은 A 또는 B 타입으로 좁혀진다
  if (param.value === “a”) {
    param.answer; // 1 | 2 return;
  }
  // 유닛 타입이 아니거나 인스턴스화할 수 있는 타입일 경우 타입이 좁혀지지 않는다
  if (typeof param.value === “string”) {
    param.answer; // 1 | 2 | 3 return;
  }
  if (param.value instanceof Error) {
    param.answer; // 1 | 2 | 3 return;
  }
  /** 판별자가 answer일 때 */
  param.value; // string | Error
  // 판별자가 유닛 타입이므로 타입이 좁혀진다
  if (param.answer === 1) {
    param.value; // ‘a’
  }
}

Exhaustiveness Checking - 정확한 타입 분기

Exhaustiveness: 철저함, 완전함
Exhaustiveness Checking: 모든 케이스에 대해 철저하게 타입을 검사며 타입 좁히기에 사용되는 패러다임 , 타입 검사를 강제하기

type ProductPrice = “10000” | “20000”;

const getProductName = (productPrice: ProductPrice): string => {
  if (productPrice === “10000”) return “배민상품권 1만 원”;
  if (productPrice === “20000”) return “배민상품권 2만 원”;
  else {
    return “배민상품권”;
  }
};

여기서 ProductPrice타입이 업데이트 해야 한다치면

type ProductPrice = “10000” | “20000” | “5000”;

const getProductName = (productPrice: ProductPrice): string => {
  if (productPrice === “10000”) return “배민상품권 1만 원”;
  if (productPrice === “20000”) return “배민상품권 2만 원”;
  if (productPrice === “5000”) return “배민상품권 5천 원”; // 조건 추가 필요
  else {
    return “배민상품권”;
  }
};

이렇게 수정하면 괜찮으나 실수로 getProductName함수에 조건 검사를 하지 않아도 에러가 발생하지 않는다
그리하여 강제하고 싶으면

type ProductPrice = “10000” | “20000” | “5000”;

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 assignable to parameter of type ‘never’
    return “배민상품권”;
  }
};

const exhaustiveCheck = (param: never) => {
  throw new Error(“type error!”);
};

exhaustiveCheck(productPrice)에서 에러를 뱉고 있는데 ProductPrice타입 중 5000이라는 값에 대한 분기 처리가 되지 않았기 때문이다.
이렇게 모든 케이스에 타입 분기 처리를 하기 위해선 exhaustiveCheck를 활용하면 되고 매개변수를 never타입을 띄우는데 never는 매개변수 어떤 값도 받을 수 없으면서 만일 값이 들어오면에러를 내뱉는다.

반응형