[5장] 타입 활용하기
5.1 조건부 타입
- 타입스크립트에서는 조건부 타입을 사용해 조건에 따라 출력 타입을 다르게 도출할 수 있다.
extends와 제네릭을 활용한 조건부 타입
- 조건부 타입에서 extends를 사용할 때는 자바스크립트 삼항 연산자와 함께 쓴다.
- 이 표현은 타입 T를 U에 할당할 수 있으면 X 타입, 아니면 Y 타입으로 결정됨을 의미한다.
T extends U ? X : Y
- 제네릭과 extends를 함께 사용해 제네릭으로 받는 타입을 제한할 수 있다.
- 따라서 개발자는 잘못된 값을 넘길 수 없기 때문에 휴먼 에러를 방지할 수 있다.
- extends를 활용해 조건부 타입을 설정할 수 있다.
- 조건부 타입을 사용해서 반환 값을 사용자가 원하는 값으로 구체화할 수 있다. 이에 따라 불필요한 타입 가드, 타입 단언 등을 방지할 수 있다.
infer를 활용해서 타입 추론하기
-
extends를 사용할 때 infer 키워드를 사용할 수 있다.
- infer는 추론하다라는 의미를 지니고 있는데 타입스크립트에서도 단어 의미처럼 타입을 추론하는 역할을 한다.
- 삼항 연산자를 사용한 조건문의 형태를 가지는데, extends로 조건을 서술하고 infer로 타입을 추론하는 방식을 취한다.
type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;
- UnpackPromise 타입은 제네릭으로 T를 받아 T가 Promise로 래핑된 경우라면 K를 반환하고, 그렇지 않은 경우에는 any를 반환한다.
Promise<infer K>
는 Promise의 반환 값을 추론해 해당 값의 타입을 K로 한다는 의미이다.
5.2 템플릿 리터럴 타입 활용하기
- 타입스크립트에서는 유니온 타입을 사용하여 변수 타입을 특정 문자열로 지정할 수 있다.
- 타입스크립트 4.1부터 이를 확장하는 방법인 템플릿 리터럴 타입(Template Literal Types)을 지원하기 시작했다.
- 템플릿 리터럴 타입은 자바스크립트의 템플릿 리터럴 문법을 사용해 특정 문자열에 대한 타입을 선언할 수 있는 기능이다.
type HeadingNumber = 1 | 2 | 3 | 4 | 5;
type HeaderTag = h${HeadingNumber};
- 주의할 점
- 타입스크립트 컴파일러가 유니온을 추론하는 데 시간이 오래 걸리면 비효율적이기 때문에 타입스크립트가 타입을 추론하지 않고 에러를 내뱉을 때가 있다.
5.3 커스텀 유틸리티 타입 활용하기
- 원하는 타입을 정확하게 설정해야만 해당 컴포넌트, 함수의 안정성과 사용성을 높일 수 있지만 타입스크립트에서 제공하는 유틸리티 타입만으로는 표현하는 데 한계를 느끼기도 한다. 이럴 때는 유틸리티 타입을 활용한 커스텀 유틸리티 타입을 제작해서 사용하면 된다.
- Pick, Omit과 같은 유틸리티 타입을 사용하면 중복된 코드를 작성하지 않고 유지보수가 더 편리한 코드를 작성할 수 있다.
PickOne 커스텀 유틸리티 타입 구현하기
- T에는 객체가 들어온다고 가정한다.
type PickOne<T> = { [P in keyof T]: Record<P, T[P]> & Partial<Record<Exclude<keyof T, P>, undefined>> }[keyof T];
NonNullable 타입 검사 함수를 사용하여 간편하게 타입 가드하기
- NonNullable 타입이란 타입스크립트에서 제공하는 유틸리티 타입으로 제네릭으로 받는 T가 null 또는 undefined일 때 never 또는 T를 반환하는 타입이다.
type NonNullable<T> = T extends null | undefined ? never : T;
- 이를 다음과 같이 사용하면 Promise에서 undefined또는 null을 제외시킬 수 있다.
const shopAdCampaignList = await Promise.all(shopList.map(shop => AdCampaignAPI.operating(shop.shopNo)));
const shopAds = shopAdCampaignList.filter(NonNullable);
5.4 불변 객체 타입으로 활용하기
- as const 키워드로 불변 객체로 선언하고, keyof 연산자를 사용하여 실제 해당 객체에 존재하는 키값만 받도록 설정할 수 있다.
keyof, typeof 활용
const theme = {
colors: {
default: colors.gray,
...colors
}
};
type ColorType = keyof typeof theme.colors;
interface Props {
color: ColorType;
}
5.5 Record 원시 타입 키 개선하기
- 객체 선언 시 키가 어떤 값인지 명확하지 않다면 Record의 키를 string이나 number 같은 원시 타입으로 명시하곤 한다.
- 이때 타입스크립트는 키가 유효하지 않더라도 타입상으로는 문제없기 때문에 오류를 표시하지 않는다.
- 이것은 예상치 못한 런타임 에러를 야기할 수 있다.
개선방법
- 옵셔널 체이닝 사용하기
foodByCategory[양식]?.map(food => console.log(food.name));
- 유닛 타입으로 변경하기
- 키가 유한한 집합이라면 유닛타입을 사용할 수 있다.
type Category = 한식 | 일식;
interface Food {
name: string;
// ...
}
const foodByCategory: Record<Category, Food[]> = {
한식: [{ name: 제육덮밥 }, { name: 뚝배기 불고기 }],
일식: [{ name: 초밥 }, { name: 텐동 }],
};
// Property 양식 does not exist on type Record<Category, Food[]>. foodByCategory[양식];
- Partial을 활용하여 포현하기
- 키가 무한한 상황에서는 Partial을 사용하여 해당 값이 undefined일 수 있는 상태임을 표현할 수 있다.
type PartialRecord<K extends string, T> = Partial<Record<K, T>>;
const foodByCategory: PartialRecord<Category, Food[]> = {
한식: [{ name: 제육덮밥 }, { name: 뚝배기 불고기 }],
일식: [{ name: 초밥 }, { name: 텐동 }],
};