좀 심화한 타입 검사에 필요한 지식들
여러 가지 타입을 결합하여 하나의 단일 타입으로 만들 수 있음
&를 사용해서 표기함. 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 타입이 됩니다.
자바스크립트 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키워드 사용하던지, 일반 함수 사용하던지!
제네릭의 장점이 뭐냐하면...
다양한 타입을 받게함으로써 코드를 효율적으로 재사용할 수 있다는 것!
그렇다면 현업에서는 언제 가장 많이 쓰일까?
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 리엑트