[우아한타입리액트] 3장. 고급 타입(2)

Lina Hongbi Ko·2024년 11월 30일
0
post-thumbnail

3장. 고급 타입 (2)

💡 타입 조합

📍 교차 타입(Intersection)

  • 교차 타입을 사용하면 여러 가지 타입을 결합해 하나의 단일 타입으로 만들 수 있음 -> 기존에 존재하는 다른 타입들을 합쳐서 해당 타입의 모든 멤버를 가지는 새로운 타입을 생성

  • 교차 타입은 &을 사용해서 표기 -> 결과물로 탄생한 단일 타입에는 타입 별칭(type alias)을 붙일 수도 있음

  • 타입 C가 타입 A와 B의 교차 타입, 즉 A & B 라면 타입 C는 타입 A와 타입 B의 모든 멤버를 가지고 있는 타입 (2개의 타입 뿐만 아니라 여러 개의 타입을 교차시킬 수도 있음)

    type ProductItem = {
        id: number;
        name: string;
        type: string;
        price: number;
        imageUrl: string;
        quantity: number;
    };
    
    type ProdocutItemWithDiscount = ProductItem & { discountAmount: number };
  • ProductItemWithDiscount 타입의 변수를 선언하고 값을 할당하면 ProductItem의 모든 멤버와 discountAmount까지 멤버로 가지게 됨

📍 유니온 타입(Union)

  • 교차 타입(A & B)이 타입 A와 타입 B를 모두 만족하는 경우라면, 유니온 타입은 타입 A 또는 타입 B 중 하나가 될 수 있는 타입을 말하며 A | B 로 표기

  • 주로 특정 변수가 가질 수 있는 타입을 전부 나열하는 용도로 사용

  • 교차 타입과 마찬가지로 2개 이상의 타입을 이어 붙일 수 있고, 타입 별칭을 통해 중복을 줄일 수도 있음

    type CardItem = {
        id: number;
        name: string;
        type: string;
        imageUrl: stirng;
    }
    type PromotionEventItem = ProductItem | CardItem;
    
    const printPromotionItem = (item: PromotionEventItem) => {
    	console.log(item.name); // ok
      
      	console.log(item.quantity); // 컴파일 에러 발생
    };
  • ProductItem 혹은 CardItem 이 될 수 있음 -> 이벤트 프로모션의 대상으로 상품이 될 수도 있고, 카드가 될 수도 있다는 의미

  • printPromotionItem() 함수를 보면 인자로 PromotionEventItem 타입을 받고 있음 -> 해당 함수 내부에서 quantity를 참조하려고 시도하면 컴파일 에러가 발생하는데, quantity가 ProductItem에만 존재하기 때문 (PromotionEventItem은 CardItem도 포함하는데 CardItem은 quantity 멤버를 가지고 있지 않기 때문에 PromotionEventItem에서는 quantity를 참조할 수 없음)

  • 교차 타입과 유니온 타입은 여러 줄에 걸쳐 표기할 수도 있는데, 이럴 경우 각 줄의 맨 앞에 & 혹은 |를 붙여서 표기

    type PromotionEventItem =
    | ProductItem
    | CardItem

📍 인덱스 시그니처(Index Signatures)

  • 인덱스 시그니처는 특정 타입의 속성 이름은 알 수 없지만 속성 값의 타입을 알고 있을 때 사용하는 문법

  • 인터페이스 내부에 [Key: K]: T 꼴로 타입을 명시해주면 되는데, 이는 해당 타입의 속성 키는 모두 K 타입이어야 하고 속성값은 모두 T 타입을 가져야 한다는 의미

    iterface IndexSignatureEx {
        [key: string]: number;
    }
  • 인덱스 시그니처를 선언할 때, 다른 속성을 추가로 명시해줄 수 있는데, 이때 추가로 명시된 속성은 인덱스 시그니처에 포함되는 타입이어야 함

    interface IndexSignatureEx2 {
        [key: string]: number | boolean;
        length: number;
        isValid: boolean;
        name: string; // 에러 발생
    }
  • name은 string 타입을 가지도록 선언되어 있지만, 인덱스 시그니처의 키가 string일 때는 number | boolean 타입이 오게끔 선언되어 있기 때문에 에러가 발생함

    📍 인덱스드 엑세스 타입(Indexed Access Types)

  • 인덱스드 엑세스 타입은 다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용됨

    type Example = {
        a: number;
        b: string;
        c: boolean;
    };
    
    type IndexedAccess = Example["a"]; // number
    type IndexedAccess2 = Example["a" | "b"]; // number | string
    type IndexedAccess3 = Example[keyof Example]; // number | string | boolean
    
    type ExAlias = "b" | "c";
    type IndexedAccess4 = Example[ExAlias]; // string | boolean
  • IndexedAccess는 Example 타입의 a 속성이 가지는 타입을 조회하기 위한 인덱스드 엑세스 타입

  • 인덱스에 사용되는 타입 또한 그 자체로 타입이기 때문에 유니온 타입, keyof, 타입 별칭 등의 표현을 사용 할 수 있음

  • 배열의 요소 타입을 조회하기 위해 인덱스드 엑세스 타입을 사용하는 경우가 있음

  • 배열 타입의 모든 요소는 전부 동일한 타입을 가지며 배열의 인덱스는 숫자 타입 -> number로 인덱싱해 배열 요소를 얻은 다음에 typeof 연산자를 붙여주면 해당 배열 요소의 타입을 가져올 수 있음

    const PromotionList = [
        { type: "product", name: "chicken" },
        { type: "product", name" "pizza" },
        { type: "card", name: "cheer-up" }
    ];
    
    type ElementOf<T> = typeof T[number];
    
    type PromotionItemType = ElemetOf<PromotionList>
    // type PromotionItemType = { type: string; name: string }

    *공부하면서 도움된 글 : https://velog.io/@orodae/%EC%9D%B8%EB%8D%B1%EC%8A%A4%EB%93%9C-%EC%97%91%EC%84%B8%EC%8A%A4-%ED%83%80%EC%9E%85

📍 맵드 타입(Mapped Types)

  • 보통 map은 유사한 형태를 가진 여러 항목의 목록 A를 변환된 항목의 목록 B로 바꾸는 것을 의미함

  • 자바스크립트의 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};
  • 맵드 타입에서 매핑할 때는 readonly와 ?를 수식어로 적용할 수 있음(readonly: 읽기 전용으로 만들고 싶을 때 붙여주는 수식어 / ?: 선택적 매개변수(옵셔널 파라미터)로 만들고 싶을 때 붙여주는 수식어)

  • 맵드 타입에서 위의 수식어를 더해주는 것뿐만 아니라 제거할 수도 있음 (기존 타입에 존재하던 readonly나 ? 앞에 -를 붙여주면 해당 수식어를 제거한 타입을 선언할 수 있음)

    type ReadOnlyEx = {
        readonly a: number;
        readonly b: string;
    };
    
    type CreateMutable<Type> = {
        -readonly [Property in keyof Type]: Type[Property];
    };
    
    type ResultType = CreateMutable<ReadOnlyEx>; // { a: number; b: string; }
    
    type OptionalEx = {
        a?: number;
        b?: string;
        c: boolean;
    };
    
    type Concrete<Type> = {
        [Propety in keyof Type]-?: Type[Property];
    };
    
    type ResultType = Concrete<OptionalEx>; // { a: number; b: string; c: boolean }
  • 배달의 민족 선물하기 서비스 '바텀시트'를 보면, 선물하기 서비스의 최근 연락처 목록, 카드 선택, 상품 선택 등 여러 지면에서 사용되고 있다. 바텀 시트마다 각각 resolver, isOpened 등의 상태를 관리하는 스토어가 필요한데 이 스토어의 타입(BottomSheetStore)을 선언해줘야 한다. 이때 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,
    };
    
    export type BOTTOM_SHEET_ID = keyof typeof BottomSheetMap;
    
    // 불필요한 반복 발생
    type BttomSheetStore = {
        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;
        };
    };
  • 맵드 타입에서는 as 키워드를 사용해 키를 재지정 할 수 있음 (BttomSheetStore의 키 이름에 BottomSheetMap의 키 이름을 그대로 쓰고 싶은 경우가 있을 수 있고, 모든 키에 _BOTTOM_SHEET를 붙이는 식으로 공통된 처리를 적용해 새로운 키를 지정하고 싶을 수도 있다)

    type BottomSheetStore = {
        [index in BOTTOM_SHEET_ID as `${index}_BOTTOM_SHEET`]: {
            resolver?: (payload: any) => void;
            args?: any;
            isOpened: boolean;
        };
    };

📍 템플릿 리터럴 타입(Template Literal Types)

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

    type Stage =
        | "init"
        | "select-iamge"
        | "edit-image"
        | "decorate-card"
        | "capture-image";
    type StageName = `${Stage}-stage`;
    // 'init-stage' | 'select-image-stage' | 'edit-image-stage' | 'decorate-card-stage' | 'capture-image-stage'
    
  • 변수 자리에 문자열 리터럴의 유니온 타입 Stage를 넣으면 해당 유니온 타입 멤버들이 차례대로 해당 변수에 들어가서 -stage가 붙은 문자열 리터럴의 유니온 타입을 결과로 반환

  • Stage 타입의 각 멤버에 -stage를 추가해 새로운 문자열 리터럴 유니온 타입을 만들어냄

📍 제네릭(Generic)

  • 제네릭은 C나 자바 같은 정적 언어에서 다양한 타입 간에 재사용성을 높이기 위해 사용하는 문법

  • 타입스크립트도 정적 타입을 가지는 언어이므로 제네릭 문법을 지원하고 있음

  • 제네릭의 사전적 의미 : 특징이 없거나 일반적인 것 (not specific, general)을 뜻함

  • 타입스크립트 제네릭도 이와 비슷한 맥락 -> 일반화된 데이터 타입

  • 타입스크립트 제네릭의 개념을 풀어보면 : 함수, 타입, 클래스 등에서 내부적으로 사용할 타입을 미리 정해두지 않고 타입 변수를 사용해서 해당 위치를 비워 둔 다음, 실제로 그 값을 사용할 때 외부에서 타입 변수 자리에 타입을 지정하여 사용하는 방식

  • 함수, 타입, 클래스등 여러 타입에 대해 하나하나 따로 정의하지 않아도 되기 때문에 재사용성 크게 향상됨

  • 타입 변수는 일반적으로 < T >와 같이 꺾쇠괄호 내부에 정의됨

  • 사용할 때는 함수에 매개변수를 넣는 것과 유사하게 원하는 타입을 넣어주면 됨

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

type ExampleArrayType<T> = T[];

const array1: ExampleArrayType<string> = ["치킨", "피자", "우동"];
  • 제네릭이 일반화된 데이터 타입을 말한다고 했는데, 이 표현만 보면 any의 쓰임과 혼동할 수도 있음 but 둘은 명확히 다름!

  • 둘의 차이는 배열을 떠올리면 쉽게 알 수 있음
    any 타입의 배열에서는 배열 요소들의 타입이 전부 같지 않을 수 있음(타입 정보를 잃어버림) => any를 사용하면 타입 검사를 하지 않고 모든 타입이 허용되는 타입으로 취급됨
    반면, 제네릭은 any처럼 아무 타입이나 무분별하게 받는게 아니라 배열 생성 시점에 원하는 타입으로 특정할 수 있음 => 배열 요소가 전부 동일한 타입이라고 보장할 수 있음

    type ExampleArrayType2 = any[];
    
    const array2: ExampleArraytype2 = [
        "치킨",
        {
            id: 0,
            name: "치킨",
            price: 20000,
            quantity: 1
        },
        99,
        true,
    ];
  • 제네릭 함수를 호출할 때 반드시 꺾쇠괄호(<>)안에 타입을 명시해야하는 것은 아님 -> 타입을 명시하는 부분을 생략하면 컴파일러가 인수를 보고 타입을 추론해줌 => 타입 추론이 가능한 경우에는 타입 명시 생략 가능

    function exampleFunc<T>(arg: T): T[] {
        return new Array(3).fill(arg);
    }
    
    exampleFunc("hello"); // T는 string으로 추론됨
  • 특정 요소 타입을 알 수 없을 때는 제네릭 타입에 기본값을 추가할 수 있음

    interface SubmitEvent<T = HTMLElement> extends SyntheticEvent<T> { submitter: T; }
  • 제네릭은 일반화된 데이터 타입을 의미 -> 함수나 클래스 등의 내부에서 제네릭을 사용할 때 어떤 타입이든 될 수 있다는 개념을 알고 있어야함 -> 특정한 타입에서만 존재하는 멤버를 참조하려고 하면 안됨!
    e.g) 배열에만 존재하는 length 속성을 제네릭에서 참조하려고 하면 에러 발생 (컴파일러는 어떤 타입이 제네릭에 전달될지 알 수 없기 때문에 모든 타입이 length 속성을 사용할 수는 없다고 알려주는 것임)

    function exampleFunc2<T>(arg: T): number {
        return arg.length; // 에러 발생: Property 'length' does not exist on type T.
    }
  • 이럴때는 제네릭 꺾쇠괄호 내부에 'length 속성을 가진 타입만 받는다'라는 제약을 걸어줌으로써 length 속성을 사용할 수 있게끔 만들 수 있음

    interface TypeWithLength {
        length: number;
    }
    
    function exampleFunc2<T extends TypeWithLength>(arg: T): number {
        return arg.length;
    }
  • 제네릭을 사용할 때 주의해야 할점 -> 파일 확장자가 tsx일때 화살표 함수에 제네릭을 사용하면 에러가 발생함

  • tsx는 타입스크립트 + JSX 이므로 제네릭의 꺾쇠괄호와 태그의 꺾쇠괄호를 혼동하여 문제가 생김
    *JSX는 HTML과 유사한 구문을 사용해 컴포넌트를 작성하는데 사용되는 기술. JSX에서는 태그를 나타내는데 꺾쇠괄호(<>)를 사용

  • 위와 같은 상황을 피하기 위해 제네릭 부분에 extends 키워드를 사용해 컴파일러에게 특정 타입의 하위 타입만 올 수 있음을 확실히 알려주면 됨 -> 보통 제네릭을 사용할 때는 function 키워드로 선언하는 경우가 많음!

// 에러 발생: JSX element 'T' has no corresponding closing tag
const arrowExampleFunc = <T>(arg: T): T[] => {
	return new Array(3).fill(arg);
};

// 에러 발생 X
const arrowExampleFunc2 = <T extends {}>(arg: T): T[] => {
	return new Array(3).fill(arg);
};

💡 제네릭 사용법

제네릭은 다양한곳에 사용할 수 있음

📍 함수의 제네릭

  • 어떤 함수의 매개변수나 반환 값에 다양한 타입을 넣고 싶을 때 사용
  • T 자리에 넣는 타입에 따라 ReadOnlyRepository가 적절하게 사용될 수 있음
function ReadOnlyRepository<T>(target: ObjectType<T> | EntitySchema<T> | string): Repository<T> {
	return getConection("ro").getRepository(target);
}

📍 호출 시그니처의 제네릭

  • 호출 시그니처(타입 시그니처)는 타입스크립트의 함수 타입 문법으로 함수의 매개변수와 반환 타입을 미리 선언하는 것을 말함
  • 호출 시그니처를 사용함으로써 개발자는 함수 호출 시 필요한 타입을 별도로 지정할 수 있게 됨
  • 호출 시그니처를 사용할 때 제네릭 타입을 어디에 위치시키는지에 따라 타입의 범위와 제네릭 타입을 언제 구체 타입으로 한정할지를 결정할 수 있음
inteface useSelectPaginationProps<T> {
	categoryAtom: RecoilState<number>;
    filterAtom: RecoilState<string[]>;
    sortAtom: RecoilState<SortType>;
    fetcherFunc: (props: CommonListRequest) => Promise<DefaultResponse<ContentListResponse<T>>>;
}

📍 제네릭 클래스

📍 제한된 제네릭

📍 확장된 제네릭

📍 제네릭 예시

🔆 제네릭을 굳이 사용하지 않아도 되는 타입

🔆 any 사용하기

🔆 가독성을 고려하지 않은 사용

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글