타입 활용하기 조건부 타입 타입스크립트의 조건부 타입은 Condition ? A : B 의 형태를 가진다.
interface Bank {
financialCode: string;
companyName: string;
name: string;
fullName: string;
}
interface Card {
financialCode: string;
companyName: string;
name: string;
appCardType?: string;
}
type PayMethod<T> = T extends "card" ? Card : Bank;
type CardPayMethodType = PayMethod<"card">;
type BankPayMethodType = PayMethod<"bank">;
PayMethod안에 제네릭 매개변수로 들어온 문자열에 따라 타입이 결정된다. 때문에 조건부 타입을 효율적으로 쓸 수 있게 된다.
extends 조건부 타입을 활용하여 개선하기
export const useGetRegisteredList = <T extends "card" | "appcard" | "bank">(
type: T
): UseQueryResult<PayMethodType<T>[]> => {
// 받아온 T에 따라 url 주소가 결정이 된다.
const url = `baeminpay/codes/${type === "appcard" ? "card" : type}`;
// fetcherFactory는 axios를 감싸고 있는 함수이다. 받아온 T에 따라 타입이 결정된다.
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
}
받아온 T
로 인해서 받아오는 타입을 정해주었다. 이로써 불필요한 타입가드 함수를 작성하지 않아도 되게 하였다.
다음과 같이 정리할 수 있다
제네릭과 extends를 함께 사용해 제네릭으로 받는 타입을 제한했다. 따라서 개발자는 잘못된 값을 넘길 수 없기 때문에 휴먼 에러를 방지할 수 있다. Extends를 활용해 조건부 타입을 설정하였다. 조건부 타입을 사용하여 반환 값을 사용자가 원하는 값으로 구체화할 수 있었다. 이에 따라 불필요한 타입 가드, 타입 단언을 방지할 수 있다.
infer를 활용하여 타입 추론하기 infer는 추론하다 라는 의미를 가지고 있다. extends로 조건을 서술하고 infer로 타입을 추론할 수 있다.
type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;
UnpackPromise
타입은 제네릭으로 T를 받아 T가 Promise로 래핑된 경우라면 K를 반환하고 그렇지 않은 경우에는 K를 반환한다.
이처럼 extends와 infer, 제네릭을 활용하면 조금 더 세밀하게 타입을 조절할 수 있다.
interface RouteBase {
name: string;
path: string;
component: ComponentType;
}
export interface RouteItem {
name: string;
path: string;
component?: ComponentType;
pages?: RouteBase[];
}
export const routes: RouteItem[] = [
{
name: "기기 내역 관리",
path: "/device-history",
component: DeviceHistoryPage,
},
{
name: "헬멧 인증 관리",
path: "/helmet-certification",
component: HelmetCertificationPage,
}
]
RouteBase와 RouteItem은 라우팅을 위해 사용하는 타입이다. routes와 같이 배열 형태로 사용되며 권한 API로 반환된 사용자 권한과 name을 비교하여 인가되지 않은 사용자 접근을 방지합니다.
export type MenuItem = MainMenu | SubMenu;
export const menuList: MenuItem[] = [
{
name: "계정 관리",
subMenus: [
{
name: "기기 내역 관리",
path: "/device-history",
},
{
name: "헬멧 인증 관리",
path: "/helmet-certification",
}
]
},
{
name: "운행 관리",
path: "/operation"
}
// ...
]
하지만 이곳에서도 문제가 있다 바로 name
속성이 문자열이면 전부 받을 수 있는 것이다. 때문에 타입을 유니온 타입으로라도 명시해주어야 한다.
export interface MainMenu {
// ...
subMenus?: ReadonlyArray<SubMenu>;
}
export const menuList = [
// ...
] as const
interface RouteBase {
name: PermissionNames;
path: string;
component: ComponentType;
}
export type RouteItem =
| {
name: string;
path: string;
component?: ComponentType;
pages: RouteBase[];
}
| {
name: PermissionNames;
path: string;
component?: ComponentType;
};
먼저 subMenus의 타입을 ReadonlyArray로 변경하고, menuList에 as const를 추가하여 불변 객체로 정의합니다. Route 관련 타입의 name은 menuList의 name에서 추출한 타입인 PermissionNames만 올 수 있도록 변경합니다.
type UnpackMenuNames<T extends ReadOnlyArray<MenuItem>> = T extends ReadonlyArray<infer U> ? U extends MainMenu
? U["subMenus"] extends infer V
? V extends ReadonlyArray<SubMenu>
? UnpackMenuName<V>
: U["name"]
: never
: U extends SubMenu
? U["name"]
: never
: never;
다음과 같은 동작을 수행한다.
U가 MainMenu 타입이라면 subMenus를 infer V로 추출한다.
subMenus는 옵셔널한 타입이기 때문에 추출한 V가 존재한다면(SubMenu 타입에 할당할 수 있다면) UnpackMenuName에 다시 전달한다.
V가 존재하지 않는다면 MainMenu의 name의 권한에 해당하므로 U["name"]이다.
U가 MainMenu가 아니라 SubMenu에 할당할 수 있다면 (U는 SubMenu 타입이기 때문에) U["name"]은 권한에 해당합니다.
export type PermissionNames = UnpackMenuNames<typeof menuList>
PermissionNames
는 menuList에서 권한으로 유효한 값만 추출하는 타입임을 확인할 수 있습니다.
템플릿 리터럴 타입 활용하기 유니온 타입을 사용하여 변수 타입을 문자열로 지정할 수 있습니다.
type HeaderTag = "h1" | "h2" | "h3" | "h4" | "h5"
이는 타입스크립트 4.1 버전부터 지원하는 템플릿 리터럴 방식이므로 이를 통해 자동 완성을 지원할 수도 있습니다.
주의할 점 타입스크립트 컴파일러가 유니온을 추론하는 데 시간이 오래 걸리면 비효율적이기 때문에 타입스크립트가 타입을 추론하지 않고 에러를 내뱉을 때가 있습니다. 따라서 템플릿 리터럴 타입에 삽입된 유니온 조합의 경우의 수가 너무 많지 않게 적절하게 나누어 타입을 정의하는 것이 좋습니다.
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Chunk = `${Digit}${Digit}${Digit}${Digit}`;
type PhoneNumberType = `010-${Chunk}-${Chunk}`;
커스텀 유틸리티 타입 활용하기 표현하기 힘든 타입을 마주할 때가 있다. 이는 안정성과 사용성을 높일 수는 있지만 ts에서 제공되는 타입만으로는 한계를 느끼기도 한다.
export type Props = {
height?: string;
color?: keyof typeof colors;
isFull?: boolean;
className?: string;
}
export const Hr: VFC<Props> = ({ height, color, isFull, className }) => {
...
return <HrComponent height={height} color={color} isFull={isFull} className={className} />;
};
// style.ts
import { Props } from '...'
type StyledProps = Pick<Props, "height" | "color" | "isFull">;
const HrComponent = styled.hr<StyledProps>`
height: ${({ height }) =>height || "10px"};
margin: 0;
background-color: ${({ color }) => colors[color || "gray7"]};
border: none;
${({ isFull }) =>
isFull && css`margin: 0 -15px`
}
`
PickOne 유틸리티함수 타입스크립트에서는 서로 다른 2개 이상의 객체를 유니온 타입으로 받을 때 타입 검사가 제대로 진행되지 않은 이슈가 있습니다.
type Card = {
card : string
}
type Account = {
account : string
}
function withdraw(type: Card | Account){
...
}
때문에 식별할 수 있는 유니온 기법을 자주 활용합니다.
type Card = {
card : string;
type : "card";
}
type Account = {
account : string;
type : "account";
}
function withdraw(type: Card | Account){
...
}
일일이 type을 다 넣어줘야하는 불편함이 생긴다. 처음부터 식별할 수 있는 유니온 형태로 설계했다면 불편함을 덜했을 수도 있지만, 이미 구현된 상태에서 적용하려면 모두 수정해야한다.
구현해야하는 타입은 다음과 같다. account 또는 card 속성 하나만 존재하는 객체를 받는 타입이다. 처음에 작성한 것처럼 | 으로 타입을 구현했을 때는 account와 card속성을 모두 가진 객체가 허용이 되었다
때문에 하나의 속성이 들어왔을 때 다른 타입을 옵셔널한 undefined 값으로 지정하는 방법에 대해서 생각해보아야 한다.
type PickOne<T> = {
[P in keyof T]: Record<P, T[P]> & Partial<Record<Exclude<keyof T, P>, undefined>>;
}[keyof T]
살펴보기 앞의 유틸리티 타입을 하나씩 뜯어보자.
type One<T> = { [P in keyof T]: Record<P, T[P]>}[keyof T];
[P in keyof T]에서 T는 객체로 가정하기 때문에 P는 T의 객체의 키 값을 말한다.
Record<P, T[P]>는 P 타입을 키로 가지고, value는 P를 키로 둔 T 객체의 값이 레코드 타입을 말한다.
따라서 { [P in keyof T] : Record<{P, T[P]}> }에서 키는 T 객체의 키 모음이고 value는 해당 키의 원본 객체 T를 말한다.
3번의 타입에서 다시 [key of] 의 키 값으로 접근하기 때문에 최종 결과는 전달받은 T와 같다.
type Card = { card: string };
const one: One<Card> = { card: "hyundai" };
type ExcludeOne<T> = { [P in keyof T]: Partial<Record<Exclude<keyof T, P>, undefined>>}[keyof T];
[P in keyof T]
에서 T는 객체로 가정하기 때문에 P는 T 객체의 키 값을 말한다.
Exclude<keyof T, P>
는 T 객체가 가진 키값에서 P 타입과 일치하는 키값을 제외한다. 이 타입을 A라고 하자.
Record<A, undefined>
는 키로 A 타입을, 값으로 undefined 타입을 갖는 레코드 타입이다. 즉 전달받은 객체 타입을 모두 {[key] : undefined
} 형태로 만든다. 이 타입을 B라고 하자.
Partial<B>
는 B 타입을 옵셔널로 만든다. 따라서 {[key]?: undefined
}와 같다.
최종적으로 [P in keyof T]
로 매핑된 타입에서 동일한 객체의 키 값인 [keyof T]
로 접근하기 때문에 4번 타입이 변환된다.
type PickOne<T> = One<T> & ExcludeOne<T>;
One<T> & Exclude<T>
는 [P in keyof T]
를 공통으로 갖기 때문에 아래와 교차된다.
[P in keyof T]: Record<P, T[P]>& Partial<Record<Exclude<keyof T, P>, undefined>>
이 타입을 해석하면 전달된 T 타입의 1개의 키는 값을 가지고 있으며 나머지 키는 옵셔널한 undefined값을 가진 객체를 의미합니다.
withdraw({ card: "hyudai", account: "hana" }) // 에러발생
NonNullable 타입 검사 함수를 사용하여 간편하게 타입가드하기 타입 가드는 ts에서 많이 사용된다. null을 가질 수 있는 값을 null 처럼 자주 사용되는 타입 가드 패턴의 하나이다.
function NonNullable<T>(value: T): value is NonNullable<T>{
return value !== null && value !== undefined;
}
Promise.all을 사용할 때 NonNullable 적용하기 Promise.all을 사용할 때 NonNullable을 적용한 예시입니다.
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);
불변 객체 타입으로 활용하기 프로젝트를 진행할 때 상숫값을 관리할 때 객체를 사용한다.
Atom 컴포넌트에서 theme style 객체 활용하기 atom 단위의 작은 컴포넌트는 폰트 크기, 색상, 배경 색상등 다양한 환경에서 사용될 수 있도록 구현해야 하는데, 이러한 설정 값들은 props로 넘겨주도록 설계합니다.
props로 직접 색상 값을 넘겨줄 수도 있지만 그렇게 하면 사용자가 모든 색상 값을 인지해야하고, 변경 사항이 생길 때 직접 값을 넣은 모든 곳을 찾아 수정해야 하는 번거로움이 생기기 때문에 변경에 취약한 상태가 됩니다.
atom 컴포넌트에서는 객체의 색상, 폰트 사이즈, 키 값을 props로 받은 뒤 theme 객체에서 값을 받아오도록 설계합니다.
keyof keyof 연산자를 사용하면 객체의 키 값을 string 또는 number 의 리터럴 유니온 타입으로 반환합니다.
interface ColorType {
red: string;
green: string;
blue: string;
}
type ColorKeyType = keyof ColorType; // 'red' | 'green' | 'blue'
typeof typeof 연산자를 활용하면 객체의 타입을 추론할 수 있습니다.
const colors = {
red: "#FF0000",
green: "#0C952A",
blue: "#1A7CFF",
}
type ColorsType = typeof colors;
객체의 타입을 활용해서 컴포넌트 구현하기
import { FC } from "react";
import styled from "styled-components";
const colors = {
black: "#000000",
gray: "#222222",
white: "#FFFFFF",
mint: "#2AC1BC"
};
const theme = {
colors: {
default: colors.gray,
...colors
},
backgroundColor: {
default: colors.white,
gray: colors.gray,
mint: colors.mint,
black: colors.black
},
fontSize: {
default: "16px",
small: "14px",
large: "18px"
}
}
type ColorType = keyof typeof theme.colors;
type BackgroundColorType = keyof typeof theme.backgroundColor;
type FontSizeType = keyof typeof theme.fontSize;
interface Props {
color?: ColorType;
backgroundColor?: BackgroundColorType;
fontSize?: FontSizeType;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>
}
const Button: FC<Props> = ({ fontSize, backgroundColor, color, children }) => {
return (
<ButtonWrap
fontSize={fontSize}
backgroundColor={backgroundColor}
color={color}>
{children}
</ButtonWrap>
)
}
const ButtonWrap = styled.button<Omit<Props, "onClick">>`
color: ${({ color }) => theme.color[color ?? "default"]};
background-color: ${({backgroundColor}) => theme.bgColor[backgroundColor ?? "default"]};
fontsize: ${({ fontSize }) => theme.fontSize[fontSize ?? "default"]};
`
Record 원시 타입 키 개선하기 무한한 키를 집합으로 가지는 Record
type Category = string
interface Food {
name: string
}
const foodByCategory: Record<Category, Food[]> = {
한식: [{ name: "제육덮밥" }, { name: "뚝배기 불고기" }],
일식: [{ name: "초밥"}, { name: "텐동" }]
}
하지만 Category 타입은 string 타입으로 Category를 Record의 키로 사용하는 FoodByCategory라는 객체는 무한한 키 집합을 가지게 됩니다.
foodByCategory["양식"]; // Food[] 추론
foodByCategory["양식"].map((food)=> console.log(food.name)); // 오류가 발생하지 않는다.
이때 자바스크립트의 옵셔널 체이닝 등을 통해 오류를 사전에 방지할 수 있다.
옵셔널 체이닝 객체의 속성을 찾을 때 중간에 null 또는 undefined가 있어도 오류 없이 안전하게 접근하는 방법 속성이 있다면 해당 값을 반환하고 존재하지 않는다면 undefined를 반환한다.
유닛 타입으로 변경하기 키는 유한한 집합이라면 유닛 타입(다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입)을 사용할 수 있다.
type Category = "한식" | "일식";
interface Food {
name: string;
}
const foodByCategory: Record<Category, Food[]> = {
한식: [{ name: "제육덮밥" }, { name: "뚝배기 불고기" }],
일식: [{ name: "초밥"}, { name: "텐동" }]
};
// Property '양식' does not exist on type 'Record<Category, Food[]>'
Partial을 활용하여 정확한 타입 표현하기 undefined일 수도 있다는 상태임을 표현할 수 있습니다.
type PartialRecord<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[key] 를 통해서 Food[] 또는 undefined로 추론하고 개발자에게 이 값은 undefined일 수도 있으니 해당 값에 대한 처리가 필요하다고 알려줄 수 있습니다.
Last modified: 13 August 2024