[우타스] 타입 조합

함민혁·2024년 8월 27일
0

우타스

목록 보기
5/5

좀 심화한 타입 검사에 필요한 지식들

교차 타입(Intersection)

여러 가지 타입을 결합하여 하나의 단일 타입으로 만들 수 있음
&를 사용해서 표기함. A&B
결과물로 탄생한 단일 타입에는 타입별칭(type alias)을 붙일 수 있음
type ProductItemWithDiscount = ProductItem & { discountAmount : number }

유니온 타입

A|B 타입 A나 B중 하나에 해당

인덱스 시그니처

특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 있을 때 사용하는 문법

interface IndexSignatureEx2 {
  [key: string]: number | boolean;
  length: number;
  isValid: boolean;
  name: string; // 에러 발생
}

인덱스 시그니처의 키가 string일 때는 number | boolean 타입이 오게끔 선언되어 있어서 에러 발생

인덱스드 엑세스 타입

다른 타입의 특정 속성이 가지는 타입을 조회,추출하기 위해 사용됨

type Person = {
  name: string;
  age: number;
  location: string;
};

// 인덱스드 엑세스 타입을 사용하여 'Person' 타입의 'name' 속성의 타입을 가져옵니다.
type NameType = Person['name']; // NameType은 string 타입이 됩니다.

맵드 타입(Mapped Types)

자바스크립트 map : 배열 A를 기반으로 새로운 배열 B를 만들어내는 배열 메서드
마찬가지로 맵드 타입은 다른 타입을 기반으로 한 타입을 선언할 때 사용하는 문법
인덱스 시그니처 문법을 사용해서 반복적인 타입 선언을 효과적으로 줄일 수 있음

type Example = {
  a: number;
  b: string;
  c: boolean;
};

type Subset<T> = {
  [K in keyof T]?: T[K];
};

const aExample: Subset<Example> = { a: 3 };
const bExample: Subset<Example> = { b: "hello" };
const acExample: Subset<Example> = { a: 4, c: true };

Subset<T>T타입의 모든 속성을 선택적으로 포함할 수 있는 새로운 타입을 만듦

맵드 타입이 실제로 사용된 예시
배민 선물하기 서비스에는 '바텀시트'라는 컴포넌트개 존재. 밑에서부터 스르륵 올라오는 모달. 이 바텀시트는 선물하기 서비스의 최근 연락처 목록,카드 선택, 상품 선택 등 여러 지면에서 사용됨. 바텀시트마다 각각 resolver, isOpened 등의 상태를 관리하는 스토어가 필요한데 이 스토어의 타입(BottomSheetMap)을 선언해줘야 함.
이때 이 타입에 존재하는 모든 키에 대해 일일이 스토어를 만들어줄 수도 있지만 불필요한 반복이 발생함.

이럴때 인덱스 시그니처 문법을 사용해서 BottomSheetMap을 기반으로 각 키에 해당하는 스토어를 선언할 수 있음.

const BottomSheetMap = {
  RECENT_CONTACTS: RecentContactsBottomSheet,
  CARD_SELECT: CardSelectBottomSheet,
  SORT_FILTER: SortFilterBottomSheet,
  PRODUCT_SELECT: ProductSelectBottomSheet,
  REPLY_CARD_SELECT: ReplyCardSelectBottomSheet,
  RESEND: ResendBottomSheet,
  STICKER: StickerBottomSheet,
  BASE: null,
};

// BOTTOM_SHEET_ID는 BottomSheetMap에서 정의된 키들 중 하나만을 허용하는 타입
export type BOTTOM_SHEET_ID = keyof typeof BottomSheetMap;

// 불필요한 반복이 발생한다
type BottomSheetStore = {
  RECENT_CONTACTS: {
    resolver?: (payload: any) => void;
    args?: any;
    isOpened: boolean;
  };
  CARD_SELECT: {
    resolver?: (payload: any) => void;
    args?: any;
    isOpened: boolean;
  };
  SORT_FILTER: {
    resolver?: (payload: any) => void;
    args?: any;
    isOpened: boolean;
  };
  // ...
};

// Mapped Types를 통해 효율적으로 타입을 선언할 수 있다
type BottomSheetStore = {
  [index in BOTTOM_SHEET_ID]: {
    resolver?: (payload: any) => void;
    args?: any;
    isOpened: boolean;
  };
};

템플릿 리터럴 타입

자바스크립트의 템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법

type Stage =
  | "init"
  | "select-image"
  | "edit-image"
  | "decorate-card"
  | "capture-image";
type StageName = `${Stage}-stage`;
// ‘init-stage’ | ‘select-image-stage’ | ‘edit-image-stage’ | ‘decorate-card-stage’ | ‘capture-image-stage’

제네릭

제네릭은 정적 언어에서 다양한 타입 간에 재사용성을 높이기 위해사용하는 문법임.
사전적 의미 : 특징이 없거나 일반적인 것

한마디로 일반화된 데이터 타입
내부적으로 사용할 타입을 미리 정해두지 않고 타입 변수를 사용해서 해당 위치를 비워둔 다음에, 실제로 그 값을 사용할 때 외부에서 타입 변수 자리에 타입을 지정하여 사용하는 방식

보통 타입 변수명으로 T(Type), E(Element), K(Key), V(Value) 등 한글자로 된 이름을 주로 사용

제네릭은 any랑 다름. any처럼 아무 타입이나 무분별하게 받는 게 아니라, 배열 생성 시점에 원하는 타입으로 특정할 수 있음. 다시 말해 제네릭을 사용하면 배열 요소가 전부 동일한 타입이라고 보장할 수 있음

제네릭 함수를 호출할 때 반드시 꺾쇠괄호(<>)안에 타입을 명시해야 하는 것은 아님. 타입을 명시하는 부분을 생략하면 컴파일러가 인수를 보고 타입을 추론해줌. 타입 추론 가능한 경우엔 생략 가능!

function identity<T>(value: T): T {
  return value;
}

// 타입을 명시하지 않음. 컴파일러가 'number' 타입으로 추론.
const num = identity(42); // num의 타입은 number

// 타입을 명시하지 않음. 컴파일러가 'string' 타입으로 추론.
const str = identity("hello"); // str의 타입은 string

제네릭에 기본 값 추가도 물론 가능!

함수나 클래스 등의 내부에서 제네릭을 사용할 때 어떤 타입이든 될 수 있다는 개념을 알고 있어야해
특정한 타입에서만 존재하는 멤버를 참조하려고 하면 안됨. 배열에만 존재하는 length 속성을 제네릭에서 참조하려고 하면 당연히 에러가 발생함. 당연히 안됨 ㅇㅇ

function exampleFunc2<T>(arg: T): number {
  return arg.length; // 에러 발생: Property ‘length’ does not exist on type ‘T’
}

제네릭 타입은 호출 시점에 어떤 구체적인 타입으로 결정되기 때문에, T에 대해 length 속성이 정의되어 있는지 여부를 컴파일러가 알 수 없음

이럴땐!.. 제네릭 꺾쇠괄호 내부에 'length속성을 가진 타입만 받는다' 라는 제약을 걸어주면 사용할 수 있음.

interface TypeWithLength {
  length: number;
}

function exampleFunc2<T extends TypeWithLength>(arg: T): number {
  return arg.length;
}

화살표 함수에 제네릭 사용하면 에러 발생함. JSX의 태그와 제네릭의 꺾쇠괄호를 혼동해버림.
extends키워드 사용하던지, 일반 함수 사용하던지!

제네릭 사용하기

  • 함수의 제네릭
  • 호출 시그니처의 제네릭
  • 제네릭 클래스
  • 제한된 제네릭
  • 확장된 제네릭

제네릭의 장점이 뭐냐하면...
다양한 타입을 받게함으로써 코드를 효율적으로 재사용할 수 있다는 것!
그렇다면 현업에서는 언제 가장 많이 쓰일까?

API 응답 값 타입을 지정할 때!

export interface MobileApiResponse<Data> {
  data: Data;
  statusCode: string;
  statusMessage?: string;
}

API응답 값에 따라 달라지는 data를 제네릭 타입 Data로 선언하고 있음
이렇게 만든 MobileApiResponse는 실제 API 응답값의 타입을 지정할때 아래처럼 사용됨

export const fetchPriceInfo = (): Promise<MobileApiResponse<PriceInfo>> => {
  const priceUrl = "https: ~~~"; // url 주소

  return request({
    method: "GET",
    url: priceUrl,
  });
};

export const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => {
  const orderUrl = "https: ~~~"; // url 주소

  return request({
    method: "GET",
    url: orderUrl,
  });
};

제네릭이 과하게 사용되면 가독성을 해치기 때문에 코드를 읽고 타입을 이해하기가 어려워짐
복잡한 제네릭은 의미 단위로 분할해서 사용하는게 좋다

.
.
.

🫠

출처: 우아한 타입스크립트 with 리엑트

profile
Born to be FE developer 🧑🏻‍💻

0개의 댓글