테크·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 자동완성이 강력해집니다.