DevFinance
테크·4 min read

TypeScript 실무 패턴 10선

실무에서 바로 쓸 수 있는 TypeScript 패턴 10가지를 코드 예제와 함께 정리했습니다.

1. Discriminated Union

API 응답 처리에 가장 유용한 패턴입니다.

type ApiResponse<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: string }
  | { status: "loading" };

function handleResponse(res: ApiResponse<User>) {
  switch (res.status) {
    case "success":
      return res.data; // data 타입 자동 추론
    case "error":
      throw new Error(res.error);
    case "loading":
      return null;
  }
}

2. Template Literal Types

동적 문자열에 타입 안전성 부여:

type EventName = `on${Capitalize<string>}`;
type CSSUnit = `${number}${"px" | "rem" | "em" | "%"}`;

const padding: CSSUnit = "16px"; // OK
const margin: CSSUnit = "1.5rem"; // OK

3. satisfies 연산자

타입 체크하면서 추론도 유지:

const routes = {
  home: "/",
  about: "/about",
  blog: "/blog",
} satisfies Record<string, string>;

// routes.home의 타입은 "/" (리터럴)

4. 브랜드 타입

원시 타입에 의미를 부여:

type UserId = string & { __brand: "UserId" };
type PostId = string & { __brand: "PostId" };

function getUser(id: UserId) { /* ... */ }

const userId = "abc" as UserId;
const postId = "xyz" as PostId;

getUser(userId); // OK
getUser(postId); // 컴파일 에러

5. Builder 패턴

복잡한 객체 생성:

class QueryBuilder<T> {
  private filters: Array<(item: T) => boolean> = [];

  where(fn: (item: T) => boolean) {
    this.filters.push(fn);
    return this; // 체이닝
  }

  execute(data: T[]): T[] {
    return data.filter((item) =>
      this.filters.every((fn) => fn(item))
    );
  }
}

const result = new QueryBuilder<User>()
  .where((u) => u.age > 20)
  .where((u) => u.role === "developer")
  .execute(users);

6. Const Assertion + as const

불변 배열/객체에서 리터럴 타입 추출:

const STATUS = ["active", "inactive", "pending"] as const;
type Status = (typeof STATUS)[number];
// type Status = "active" | "inactive" | "pending"

7. 조건부 타입으로 API 응답 매핑

type Endpoint = "/users" | "/posts" | "/comments";

type ResponseOf<E extends Endpoint> =
  E extends "/users" ? User[] :
  E extends "/posts" ? Post[] :
  E extends "/comments" ? Comment[] :
  never;

async function fetchApi<E extends Endpoint>(
  endpoint: E
): Promise<ResponseOf<E>> {
  const res = await fetch(endpoint);
  return res.json();
}

const users = await fetchApi("/users"); // User[] 타입

8. Zod로 런타임 + 타입 동시 검증

import { z } from "zod";

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive(),
});

type User = z.infer<typeof UserSchema>;

function createUser(input: unknown): User {
  return UserSchema.parse(input); // 런타임 검증 + 타입 보장
}

9. 제네릭 컴포넌트

React에서 타입 안전한 재사용 컴포넌트:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// 사용 시 T가 자동 추론됨
<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

10. Type Guard 함수

런타임 타입 검사를 타입 시스템에 연결:

function isNonNull<T>(value: T | null | undefined): value is T {
  return value != null;
}

const results = [1, null, 3, undefined, 5];
const valid = results.filter(isNonNull);
// valid: number[] (null/undefined 제거됨)

결론

TypeScript의 힘은 런타임 에러를 컴파일 타임에 잡는 것입니다. 위 패턴들을 일상적으로 사용하면 버그가 줄고, IDE 자동완성이 강력해집니다.