우아한 타입스크립트 스터디 5장 - 타입 활용하기

우아한 타입스크립트 스터디 5장 - 타입 활용하기

개요

5장은 타입스크립트의 고급 기능을 실무에 활용하는 방법을 다룹니다. 우아한형제들(배달의민족)의 실제 개발 사례를 통해 조건부 타입, 템플릿 리터럴, 커스텀 유틸리티 타입 등 실무에서 유용한 타입 활용법을 소개합니다.


5.1 조건부 타입 (Conditional Types)

5.1.1 extends와 제네릭을 활용한 조건부 타입

기본 문법: T extends U ? X : Y

  • 타입 T를 U에 할당할 수 있으면 X 타입, 아니면 Y 타입으로 결정됩니다.
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 CardPayMethod = PayMethod<"card"> // Card 타입
type BankPayMethod = PayMethod<"bank"> // Bank 타입

장점:

  • 제네릭과 extends를 함께 사용해 타입을 제한하여 잘못된 값 방지
  • 조건부 타입으로 반환 값을 구체화하여 불필요한 타입 가드, 타입 단언 방지

5.1.2 infer를 활용한 타입 추론

extends와 함께 infer 키워드를 사용하여 타입을 추론할 수 있습니다.

// Promise 배열에서 내부 타입 추론
type UnpackPromiseArray<T> = T extends Promise<infer K>[] ? K : any

const promises = [Promise.resolve("Mark"), Promise.resolve(38)]
type Expected = UnpackPromiseArray<typeof promises> // string | number

5.2 템플릿 리터럴 타입 활용하기

자바스크립트의 템플릿 리터럴 문법을 사용해 특정 문자열 패턴에 대한 타입을 선언할 수 있습니다.

type HeadingNumber = 1 | 2 | 3 | 4 | 5
type HeaderTag = `h${HeadingNumber}` // "h1" | "h2" | "h3" | "h4" | "h5"

주의사항:

  • 유니온 조합의 경우의 수가 너무 많으면 타입스크립트 컴파일러 성능에 영향
  • 적절하게 나누어 타입을 정의하는 것이 좋음

5.3 커스텀 유틸리티 타입 활용하기

5.3.1 styled-components의 중복 타입 선언 피하기

Pick 유틸리티 타입을 활용해 Props에서 필요한 부분만 선택하여 styled-components 타입 정의:

// HrComponent.tsx
export type Props = {
  height?: string;
  color?: keyof typeof colors;
  isFull?: boolean;
  className?: string;
  // ...
};

export const Hr: VFC<Props> = ({height, color, isFull, className}) => {
  return <HrComponent ... />;
};

// style.ts
import {Props} from "...";

type StyledProps = Pick<Props, "height" | "color" | "isFull">;

const HrComponent = styled.hr<StyledProps>`
  // ...
`;

5.3.2 PickOne 유틸리티 함수

서로 다른 객체를 유니온 타입으로 받을 때의 타입 검사 문제를 해결하는 커스텀 유틸리티 타입:

문제 상황:

type Card = { card: string }
type Account = { account: string }

function withdraw(type: Card | Account) {
  /* */
}

// 타입 에러가 발생하지 않는 문제
withdraw({ card: "hyundai", account: "hana" })

PickOne 구현:

type PickOne<T> = {
  [P in keyof T]: Record<P, T[P]> &
    Partial<Record<Exclude<keyof T, P>, undefined>>
}[keyof T]

// 사용 예시
type PayMethodType = PickOne<Card & Account>
// { card: string; account?: undefined } | { card?: undefined; account: string }

5.3.3 NonNullable 타입 검사 함수

null이나 undefined를 안전하게 처리하는 타입 가드 함수:

function NonNullable<T>(value: T): value is NonNullable<T> {
  return value !== null && value !== undefined
}

// 사용 예시
const values = [1, 2, null, 4, undefined]
const validValues = values.filter(NonNullable) // number[]

5.4 불변 객체 타입으로 활용하기

5.4.1 Atom 컴포넌트에서 theme style 객체 활용

as const와 keyof 활용:

const colors = {
  red: "#F45452",
  green: "#0C952A",
  blue: "#1A7CFF",
} as const

const getColorHex = (key: keyof typeof colors) => colors[key]

// 자동완성과 타입 안전성 확보
getColorHex("red") // ✅ 정상
getColorHex("yellow") // ❌ 타입 에러

keyof 연산자:

interface ColorType {
  red: string
  green: string
  blue: string
}

type ColorKeyType = keyof ColorType // 'red' | 'green' | 'blue'

typeof 연산자:

const colors = {
  red: "#F45452",
  green: "#0C952A",
  blue: "#1A7CFF",
}

type ColorsType = typeof colors
/*
{
  red: string;
  green: string;
  blue: string;
}
*/

5.5 Record 원시 타입 키 개선하기

5.5.1 무한한 키를 가지는 Record의 문제점

type Category = string
interface Food {
  name: string
}

const foodByCategory: Record<Category, Food[]> = {
  한식: [{ name: "김치" }, { name: "된장찌개" }],
  일식: [{ name: "초밥" }, { name: "텐동" }],
}

// 런타임 에러 발생 가능
foodByCategory["양식"].map((food) => console.log(food)) // undefined.map() 에러

5.5.2 유닛 타입으로 변경

type Category = "한식" | "일식" // 유한한 집합으로 제한

const foodByCategory: Record<Category, Food[]> = {
  한식: [{ name: "김치" }, { name: "된장찌개" }],
  일식: [{ name: "초밥" }, { name: "텐동" }],
}

5.5.3 Partial을 활용한 정확한 타입 표현

키가 무한한 상황에서 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 타입으로 추론

핵심 포인트

  1. 조건부 타입으로 타입 안전성과 코드 간결성 확보
  2. infer 키워드로 복잡한 타입에서 필요한 부분만 추론
  3. 템플릿 리터럴 타입으로 문자열 패턴 타입 안전성 확보
  4. 커스텀 유틸리티 타입으로 반복되는 타입 패턴 해결
  5. 불변 객체와 keyof/typeof 조합으로 런타임 안전성 확보
  6. Record 타입 개선으로 undefined 처리 명확화

실무 적용 팁

  • 타입 가드 함수를 적극 활용하여 런타임 안전성 확보
  • Pick, Omit 등 유틸리티 타입으로 코드 중복 제거
  • as constkeyof 조합으로 자동완성과 타입 안전성 동시 확보
  • PartialRecord 패턴으로 동적 키 상황에서 안전한 타입 설계