교차 타입을 사용하면 여러 가지 타입을 결합해 하나의 단일 타입으로 만들 수 있음 -> 기존에 존재하는 다른 타입들을 합쳐서 해당 타입의 모든 멤버를 가지는 새로운 타입을 생성
교차 타입은 &을 사용해서 표기 -> 결과물로 탄생한 단일 타입에는 타입 별칭(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까지 멤버로 가지게 됨
교차 타입(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
인덱스 시그니처는 특정 타입의 속성 이름은 알 수 없지만 속성 값의 타입을 알고 있을 때 사용하는 문법
인터페이스 내부에 [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 타입이 오게끔 선언되어 있기 때문에 에러가 발생함
인덱스드 엑세스 타입은 다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용됨
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
보통 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;
};
};
템플릿 리터럴 타입은 자바스크립트의 템플릿 리터럴 문자열을 사용해 문자열 리터럴 타입을 선언할 수 있는 문법
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를 추가해 새로운 문자열 리터럴 유니온 타입을 만들어냄
제네릭은 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);
};
제네릭은 다양한곳에 사용할 수 있음
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>>>;
}