우아한형제들 웹프론트엔드개발그룹, 『우아한 타입스크립트 with 리액트』, 한빛미디어(2023) p.150 ~ 182
Condition ? A : B 형태
타입을 확장할 때와 타입을 조건부로 설정할 때 사용되며, 제네릭 타입에서는 한정자 역할로도 사용된다.
T extends U ? X : Y
타입 T를 U에 확장할 수 있으면 X 타입, 아니면 Y 타입으로 결정됨.
✏️ 제네릭(Generics)
타입을 마치 함수의 파라미터처럼 사용하는 것을 의미
type PayMethodType<T extends "card" | "appcard" | "bank"> = T extends
| "card"
| "appcard"
? Card
: Bank;
export const useGetRegisteredList = <T extends "card" | "appcard" | "bank">(
type: T
): useQueryResult<PayMethodType<T>[]> => {
const url = `baeminpay/codes/${type === "appcard" ? "card" : type}`;
const fetcher = fetcherFactory<PayMethodtype<T>[]>({
onSuccess: (res) => {
const usablePocketList =
res.filter(
(pocket: PocketInfo<Card> | PocketInfo<Bank>) =>
pocket?.useType === "USE"
) ?? [];
return usablePocketList;
},
});
};
const result = useCommonQuery<PayMethodType<T>[]>(url, undefined, fetcher);
return result;
- useCommonQuery는 useQuery를 한 번 래핑해서 사용하고 있는 함수로 반환 data를 T타입으로 반환한다.
- fetcherFactory는 axios를 래핑해주는 함수이며, 서버에서 데이터를 받아온 후 on-Success 콜백 함수를 거친 결괏값을 반환한다.
삼항 연사자를 사용한 조건문 형태, extends로 조건을 서술하고 infer로 타입을 추론하는 방식을 취함.
type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;
- UnpackPromise 타입은 제네릭으로 T를 받아 T가 Promise로 래핑된 경우라면 K를 반환하고, 그렇지 않은 경우에는 any를 반환한다.
Promise<infer K>
= Promise 반환 값을 추론해 해당 값의 타입을 K로 한다는 의미.
const promises = [Promise.resolve("Mark"), Promise.resolve(38)];
type Expected = UnpackPromise<typeof promises>; // string | number
타입스크립트에서는 유니온 타입을 사용하여 변수 타입을 특정 문자열로 지정할 수 있다.
템플릿 리터럴 타입은 JS의 템플릿 리터럴 문법을 사용해 특정 문자열에 대한 타입을 선언할 수 있는 기능이다.
type HeadingNumber = 1 | 2 | 3 | 4 | 5;
type HeaderTag = `h${HeadingNumber}`;
템플릿 리터럴을 사용하면 더욱 읽기 쉬운 코드로 작성할 수 있게 되며, 코드를 재사용하고 수정하는 데 용이한 타입을 선언할 수 있다.
⚠ 주의할 점
타입스크립트 컴파일러가 유니온을 추론하는 데 시간이 오래 걸리면 비효율적이기 때문에 타입스크립트가 타입을 추론하지 않고 에러를 내뱉을 때가 있다. 따라서 템플릿 리터럴 타입에 삽입된 유니온 조합의 경우의 수가 너무 많지 않게 적절하게 나누어 타입을 정의하는 것이 좋다.아래 코드 예시에서 Chunk는 10^4개의 경우의 수를 가지고, PhoneNumberType은 그럼 10000^2개의 경우의 수를 가지는 유니온 타입이 되기 때문에 타입스크립트에서 에러가 발생할 수 있다.
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Chunk = `${Digit}${Digit}${Digit}${Digit}`;
type PhoneNumberType = `010-${Chunk}-${Chunk}`;
타입스크립트에서 제공하는 유틸리티 타입만으로 표현하는 데 한계를 느낀다면 커스텀 유틸리티 타입을 제작해 사용.
타입스크립트에서는 서로 다른 2개 이상의 객체를 유니온 타입으로 받을 때 타입 검사가 제대로 진행되지 않는 이슈가 있다 -> 유니온은 합집합이 되기 때문에 두 객체 모두 포함되어도 합집합의 범주에 들어가기 때문에 타입 에러가 발생하지 않는다.
식별할 수 있는 유니온(Discriminated Unions)은 각 타입에 type이라는 공통된 속성을 추가하여 구분짓는 방법.
type Card = {
type: "card";
card: string;
}
type Account = {
type: "account";
account: string;
}
function withdraw(type: Card | Account) {
...
}
withdraw({type: "card", card: "hyundai"})
withdraw({type: "account", card: "hana"})
문제 해결은 됐지만 일일이 type을 다 넣어줘야 하는 불편함이 생긴다.
이러한 상황을 방지하기 위해 PickOne이라는 유틸리티 타입을 적용한다.
선택하고자 하는 하나의 속성을 제외한 나머지 값을 옵셔널 타입 + undefined로 설정하면 원하고자 하는 속성만 받도록 구현할 수 있다.
type PayMethod =
| { account: string; card?: undefined; payMoney?: undefined }
| { account: undefined; card?: string; payMoney?: undefined }
| { account: undefined; card?: undefined; payMoney?: string };
구현하고자 하는 타입 -> account 혹은 card 속성 하나만 존재하는 객체를 받는 타입
{accout: string; card?: undefined} | {account?: undefined; card: string}
type PickOne<T> = {
[P in keyof T]: Record<P, T[P]> & Partial<Record<Exclude<keyof T, P>, undefined>>;
}[key of T];
커스텀 유틸리티 타입 구현시
일반적으로 if문을 사용해서 null 처리 타입 가드를 적용하지만, is 키워드와 NonNullable 타입으로 타입 검사를 위한 유틸 함수를 만들어서 사용할 수도 있다.
NonNullable 타입이란?
타입스크립트에서 제공하는 유틸리티 타입으로 제네릭으로 받는 T가 null 또는 undefined일 때 never 또는 T를 반환하는 타입이다. 이를 활용하면 null이나 undefined가 아닌 경우를 제외할 수 있다.
type NonNullable<T> = T extends null | undefined ? never : T;
function NonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
NonNullable 함수는 매개변수인 value가 null 또는 undefined라면 false를 반환한다.
is 키워드가 쓰였기 때문에 NonNullable 함수를 사용하는 쪽에서 true가 반환된다면 넘겨준 인자는 null이나 undefined가 아닌 타입으로 타입 가드가 된다.
여러 상품의 광고를 조회시 하나의 API에서 에러가 발생한다고 해서 전체 광고가 보이지 않으면 안되므로 에러 발생시 null을 반환해준다.
class AdCampaignAPI {
static async operating(shopNo: number): Promise<AdCampaign[] {
try {
retrun await fetch(`ad/shopNumber-${shopNo}`);
} catch (error) {
return null;
}
}>
}
const shopList = [
{shopNo: 100, category: "chicken"},
{shopNo: 101, category: "pizza"},
{shopNo: 102, category: "noodle"},
];
const shopAdCampaignList = await Promise.all(shopList.map((shop) => AdCampaignAPI.operating(shop.shopNo))
);
AdCampaignAPI.operating 함수에서 null을 반환할 수 있기 때문에 shopAdCampaignList 타입은 Array<AdCampaign[] | null>로 추론된다.
const shopList = [
{ shopNo: 100, category: "chicken" },
{ shopNo: 101, category: "pizza" },
{ shopNo: 102, category: "noodle" },
];
const shopAdCampaignList = await Promise.all(
shopList.map((shop) => AdCampaignAPI.operating(shop.shopNo))
);
const shopAds = shopAdCampaignList.filter(NonNullable);
위와 같이 NonNullable를 사용해 shopAdCampaignList를 필터링하면 shopAds는 원하는 타입인 Array<AdCampaign[]>로 추론할 수 있게 된다.
상숫값을 관리할 때 객체를 사용한다. (ex_프로젝트 전체적인 theme 객체, 자주 사용하는 애니메이션을 모아둔 객체, 상숫값을 담은 객체 등)
컴포넌트나 함수에서 이런 객체를 사용할 때 열린 타입으로 설정할 수 있다.
const colors = {
red: "#F45452",
green: "#0C952A",
bule: "#1A7CFF",
};
const getColorHex = (key: string) => colors[key];
위 코드에서 키 타입을 해당 객체에 존재하는 키값으로 설정하는 것이 아니라 string으로 설정하면 getColorHex 함수의 반환 값은 any가 된다. colors에 어떤 값이 추가될지 모르기 때문.
여기서 as const 키워드로 객체를 불변 객체로 선언하고, keyof 연산자를 사용하여 getColorHex 함수 인자로 실제 Colors 객체에 존재하는 키값만 받도록 설정할 수 있다.
keyof, as const로 객체 타입을 구체적으로 설정하면 타입에 맞지 않는 값을 전달할 경우 타입 에러가 반환되기 때문에 컴파일 단계에서 발생할 수 있는 실수를 방지할 수 있다. 또한 자동 완성 기능을 통해 객체에 어떤 값이 있는지 쉽게 파악할 수 있게 된다.
keyof 연산자
: 객체 타입을 받아 해당 객체의 키값을 string 또는 number의 리터럴 유니온 타입을 반환한다.
interface ColorType {
red: string;
green: string;
blue: string;
}
type ColorKeyType = keyof ColorType; // 'red' | 'green' | 'blue'
const colors = {
red: "#F45452",
green: "#0C952A",
blue: "#1A7CFF",
};
type ColorsType = typeof colors;
/**
{
red: string,
green: string,
blue: string,
}
**/
Record를 명시적으로 사용하는 방안
옵셔널 체이닝 사용해 런타임 에러 방지 가능.
옵셔널 체이닝(optional chaining)
객체의 속성을 찾을 때 중간에 null 또는 undefined가 있어도 오류 없이 안전하게 접근하는 방법
?. 문법으로 표현되며 옵셔널 체이닝을 사용할 때 중간에 null 또는 undefined인 속성이 있는지 검사한다. 속성이 존재하면 해당 값을 반환하고, 존재하지 않으면 undefined를 반환한다.
키가 유한한 집합이라면 유닛 타입 사용 가능.
유닛 타입 = 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입
키가 무한한 상황에서는 Partial를 사용하여 해당 값이 undefined일 수 있는 상태임을 표현할 수 있다. 객체 값이 undefined일 수 있는 경우에 Partial를 사용해서 PartialRecord타입을 선언하고 객체를 선언할 때 이것을 활용할 수 있다.
type PartailRecord<K extends string, T> = Partial<Record<K, T>>;
type Category = string;
interface Food {
name: string;
// ....
}
const foodByCategory: PartialRecord<Category, Food[]> = {
한식: [{name: '제육덮밥'}, {name: '뚝배기 불고기'}],
일식: [{name: '초밥'}, {name: '텐동'}],
};
foodByCategory["양식"] // Food[] 또는 undefined 타입으로 추론
foodByCategory["양식"].map((food) => console.log(food)); // Object is possibly 'undefined'
foodByCategory["양식"]?.map((food) -> console.log(food.name)); // OK
개발자에게 undefined일 수 있으니 해당 값에 대한 처리가 필요하다고 표시해준다.
유틸리티 타입이란?
: 기존 타입을 변형하거나 재사용하기 쉽게 만들어주는 타입스크립트의 내장 타입.이러한 타입들은 코드의 반복을 줄이고, 타입 안전성을 유지하며, 더 간결하고 읽기 쉬운 타입 정의를 가능하게 한다. 유틸리티 타입은 기본적으로 타입스크립트의 타입 연산자를 활용하여 새로운 타입을 생성하는 기능을 제공하며, 타입 시스템을 더 유연하고 강력하게 만들어준다.
utility = 다용도의(다목적의)
Partial<T>
: T의 모든 속성을 선택적으로 만드는 타입입니다.
Required<T>
: T의 모든 속성을 필수로 만드는 타입입니다.
Readonly<T>
: T의 모든 속성을 읽기 전용으로 만드는 타입입니다.
Record<K, T>
: 키가 K이고 값이 T인 객체 타입을 정의합니다.
Pick<T, K>
: T에서 K에 해당하는 속성만을 선택하여 새로운 타입을 만듭니다.
Omit<T, K>
: T에서 K에 해당하는 속성을 제외하여 새로운 타입을 만듭니다.
Exclude<T, U>
: T에서 U에 해당하는 타입을 제외합니다.
Extract<T, U>
: T에서 U에 해당하는 타입만을 추출합니다.
NonNullable<T>
: T에서 null과 undefined를 제외합니다.
ReturnType<T>
: 함수 타입 T의 반환 타입을 추출합니다.
InstanceType<T>
: 생성자 함수 타입 T의 인스턴스 타입을 추출합니다.
ThisType<T>
: 객체 리터럴의 this 컨텍스트 타입을 지정합니다. 주로 noImplicitThis 옵션과 함께 사용됩니다.
타입스크립트에서 오류를 표시하지 않는다고 열린 타입을 프로젝트에서 많이 사용했다는 것을 알게되었다. keyof, as const로 좀 더 정확하고 안전하게 타입 작성하는 법을 배웠다. 필수로 꼭 반환값을 써줘야만 하는 타입이 아니면 생략하기도 했는데 항상 정확한 타입을 작성할 수 있도록 좀 더 주의를 기울여야겠다.
if 조건문이나 옵셔널 체이닝 사용도 많이 하는데 어떤 타입 가드가 좋은 방식인지 고민이 필요해 보인다.
책의 5장까지 와서 PickOne 커스텀 유틸리티 타입이 나오니 포기할 뻔했는데 책에 단계별 설명을 한 번 읽고 넘어갈 수 있었다. 너무 복잡해보여도 당장 100% 이해하려고 하지 말고 일단 최대한 쪼개서 한 번 이해하고 넘어가자.
type PickOne<T> = {
[P in keyof T]: Record<P, T[P]> & Partial<Record<Exclude<keyof T, P>, undefined>>;
}[key of T];
다양한 유틸리티 타입에 대해 배울 수 있었다. 유틸리티 타입 잘 활용하면 매우 유용한 것 같다.