zero-wiki Help

타입 활용하기

조건부 타입

타입스크립트의 조건부 타입은 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 로 인해서 받아오는 타입을 정해주었다. 이로써 불필요한 타입가드 함수를 작성하지 않아도 되게 하였다.

다음과 같이 정리할 수 있다

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];
  1. [P in keyof T]에서 T는 객체로 가정하기 때문에 P는 T의 객체의 키 값을 말한다.

  2. Record<P, T[P]>는 P 타입을 키로 가지고, value는 P를 키로 둔 T 객체의 값이 레코드 타입을 말한다.

  3. 따라서 { [P in keyof T] : Record<{P, T[P]}> }에서 키는 T 객체의 키 모음이고 value는 해당 키의 원본 객체 T를 말한다.

  4. 3번의 타입에서 다시 [key of] 의 키 값으로 접근하기 때문에 최종 결과는 전달받은 T와 같다.

type Card = { card: string }; const one: One<Card> = { card: "hyundai" };
  • ExcludeOne<T>

type ExcludeOne<T> = { [P in keyof T]: Partial<Record<Exclude<keyof T, P>, undefined>>}[keyof T];
  1. [P in keyof T] 에서 T는 객체로 가정하기 때문에 P는 T 객체의 키 값을 말한다.

  2. Exclude<keyof T, P>는 T 객체가 가진 키값에서 P 타입과 일치하는 키값을 제외한다. 이 타입을 A라고 하자.

  3. Record<A, undefined>는 키로 A 타입을, 값으로 undefined 타입을 갖는 레코드 타입이다. 즉 전달받은 객체 타입을 모두 {[key] : undefined} 형태로 만든다. 이 타입을 B라고 하자.

  4. Partial<B>는 B 타입을 옵셔널로 만든다. 따라서 {[key]?: undefined }와 같다.

  5. 최종적으로 [P in keyof T]로 매핑된 타입에서 동일한 객체의 키 값인 [keyof T]로 접근하기 때문에 4번 타입이 변환된다.

type PickOne<T> = One<T> & ExcludeOne<T>;
  1. One<T> & Exclude<T>[P in keyof T]를 공통으로 갖기 때문에 아래와 교차된다.

  2. [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)); // 오류가 발생하지 않는다.

이때 자바스크립트의 옵셔널 체이닝 등을 통해 오류를 사전에 방지할 수 있다.

유닛 타입으로 변경하기

키는 유한한 집합이라면 유닛 타입(다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입)을 사용할 수 있다.

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