typescript
TypeScript 타입 시스템 — Generic, Utility Types, Discriminated Union, Type Narrowing, Branded Type, Zod 스키마 검증, tsconfig 설정. Use when 복잡한 타입을 설계하거나 타입 안전성을 높이고 tsconfig 를 정할 때.
SKILL.mdplugins/frontend/skills/typescript/SKILL.md
GitHubTypeScript 심화 가이드
TypeScript 5.x 타입 시스템 심화, 제네릭 패턴, Utility Types, 타입 안전성 종합 가이드.
Quick Reference
타입 정의 선택
│
├─ 객체 형태 정의 ────────> interface (extends로 확장)
├─ Union / 조합 타입 ────> type (|, &, 조건부)
├─ 열거값 ───────────────> const enum 또는 as const
│
타입 안전성 강화
│
├─ 런타임 검증 ──────────> Zod + z.infer<typeof schema>
├─ 상태머신 ─────────────> Discriminated Union
├─ 식별 가능한 원시값 ──> Branded Types
└─ 타입 좁히기 ──────────> type guards + in + instanceof
TypeScript 5.x 주요 기능
const Type Parameters
제네릭 인자를 리터럴 타입으로 추론.
// const 없이: string[]으로 추론
function getRoutes<T extends readonly string[]>(routes: T) {
return routes;
}
const r1 = getRoutes(['home', 'about']); // string[]
// const 사용: 리터럴 튜플로 추론
function getRoutes<const T extends readonly string[]>(routes: T) {
return routes;
}
const r2 = getRoutes(['home', 'about']); // readonly ["home", "about"]
satisfies 연산자
타입 호환성 검증 + 리터럴 타입 유지.
type Color = 'red' | 'green' | 'blue';
type ColorMap = Record<Color, string | [number, number, number]>;
// satisfies: 타입 검증하면서 추론된 타입 유지
const palette = {
red: [255, 0, 0],
green: '#00ff00',
blue: [0, 0, 255],
} satisfies ColorMap;
// palette.green은 string으로 추론 (toUpperCase 사용 가능)
palette.green.toUpperCase(); // OK
// 타입 어노테이션이었다면 string | [number, number, number]로 추론
// const palette: ColorMap = { ... };
// palette.green.toUpperCase(); // Error!
Decorators (Stage 3)
// ClassMethodDecoratorContext 기반 데코레이터
function logged(target: any, context: ClassMethodDecoratorContext) {
return function(this: any, ...args: any[]) {
console.log(`Calling ${String(context.name)} with`, args);
return target.call(this, ...args);
};
}
class Calculator { @logged add(a: number, b: number) { return a + b; } }
type vs interface
| 특성 | interface | type |
|---|---|---|
| 객체 형태 | O | O |
| extends (상속) | O (빠름) | X (&로 교차) |
| implements | O | O |
| Declaration Merging | O | X |
| Union / Intersection | X | O |
| Mapped Types | X | O |
| Conditional Types | X | O |
// interface: 객체 형태 정의, 확장이 필요할 때
interface User { id: string; name: string; email: string; }
interface AdminUser extends User { role: 'admin'; permissions: string[]; }
// type: Union, 조합, 유틸리티 타입
type Status = 'active' | 'inactive' | 'suspended';
type ApiResponse<T> = { data: T; error: null } | { data: null; error: string };
권장: 객체 형태는 interface, 나머지는 type 사용.
고급 타입
Conditional Types
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// 실용적 예시: API 응답 타입 추출
type ExtractData<T> = T extends { data: infer D } ? D : never;
type UserResponse = { data: User; status: number };
type Extracted = ExtractData<UserResponse>; // User
// Promise unwrap
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Result = Awaited<Promise<Promise<string>>>; // string
Mapped Types
// 모든 프로퍼티를 선택적으로
type Optional<T> = { [K in keyof T]?: T[K] };
// 모든 프로퍼티를 읽기 전용으로
type Immutable<T> = { readonly [K in keyof T]: T[K] };
// 키 이름 변환 (Template Literal + Mapped)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
Template Literal Types
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// API 경로 타입
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'products' | 'orders';
type ApiPath = `/api/${ApiVersion}/${Resource}`;
// '/api/v1/users' | '/api/v1/products' | ... (6개 조합)
// CSS 유닛 타입
type CSSUnit = 'px' | 'rem' | 'em' | '%';
type CSSValue = `${number}${CSSUnit}`;
const margin: CSSValue = '16px'; // OK
Generic 패턴
Factory 패턴
interface Repository<T, ID = string> {
findById(id: ID): Promise<T | null>;
findAll(filter?: Partial<T>): Promise<T[]>;
create(data: Omit<T, 'id' | 'createdAt'>): Promise<T>;
update(id: ID, data: Partial<T>): Promise<T>;
delete(id: ID): Promise<void>;
}
// 구현
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> {
return await db.user.findUnique({ where: { id } });
}
// ...
}
Builder 패턴
class QueryBuilder<T> {
private filters: Record<string, unknown> = {};
private sortField?: keyof T;
where<K extends keyof T>(field: K, value: T[K]): this {
this.filters[field as string] = value;
return this;
}
orderBy(field: keyof T): this {
this.sortField = field;
return this;
}
}
// 타입 안전한 쿼리 빌더
const query = new QueryBuilder<User>()
.where('status', 'active') // OK: 'status' is keyof User
.where('age', 25) // OK: age is number
.orderBy('name');
Generic Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 복합 제약: 여러 interface를 교차
function merge<T extends HasId & HasTimestamp>(entity: T, update: Partial<Omit<T, 'id'>>): T {
return { ...entity, ...update, updatedAt: new Date() };
}
Utility Types 심화
type UpdateUser = Partial<User>; // 모든 프로퍼티 선택적
type StrictConfig = Required<Config>; // 모든 프로퍼티 필수
type UserSummary = Pick<User, 'id' | 'name'>; // 특정 프로퍼티만 선택
type CreateUser = Omit<User, 'id' | 'createdAt'>; // 특정 프로퍼티 제외
type StatusMap = Record<Status, { label: string; color: string }>; // 키-값 매핑
type OnlyStrNum = Extract<string | number | boolean, string | number>; // string | number
type OnlyBool = Exclude<string | number | boolean, string | number>; // boolean
type DefiniteUser = NonNullable<User | null | undefined>; // User
type FetchResult = ReturnType<typeof fetchUser>; // Promise<User>
type FetchParams = Parameters<typeof fetchUser>; // [id: string]
Discriminated Unions
상태머신이나 다형성을 타입으로 안전하게 모델링.
// 주문 상태를 Discriminated Union으로 모델링
type Order =
| { status: 'pending'; createdAt: Date }
| { status: 'confirmed'; confirmedAt: Date; estimatedDelivery: Date }
| { status: 'shipped'; trackingNumber: string; shippedAt: Date }
| { status: 'delivered'; deliveredAt: Date }
| { status: 'cancelled'; reason: string; cancelledAt: Date };
function getOrderMessage(order: Order): string {
switch (order.status) {
case 'pending':
return '주문이 접수되었습니다';
case 'confirmed':
return `배송 예정일: ${order.estimatedDelivery.toLocaleDateString()}`;
case 'shipped':
return `송장번호: ${order.trackingNumber}`;
case 'delivered':
return `배송 완료: ${order.deliveredAt.toLocaleDateString()}`;
case 'cancelled':
return `취소 사유: ${order.reason}`;
}
// exhaustive check: 새 status 추가 시 컴파일 에러 발생
const _exhaustive: never = order;
return _exhaustive;
}
API 응답 패턴
type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: { code: string; message: string } };
// 사용 시 타입이 자동으로 좁혀짐
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // User 타입 확정
} else {
console.error(result.error.message); // Error 타입 확정
}
Type Narrowing
// Type Guard 함수
function isAdmin(user: User | AdminUser): user is AdminUser {
return 'permissions' in user;
}
// in 연산자
function processResponse(res: SuccessResponse | ErrorResponse) {
if ('data' in res) {
return res.data; // SuccessResponse
}
return res.error; // ErrorResponse
}
// instanceof
function handleError(err: unknown) {
if (err instanceof ValidationError) {
return { status: 400, message: err.details };
}
if (err instanceof NotFoundError) {
return { status: 404, message: err.message };
}
return { status: 500, message: 'Internal error' };
}
// typeof
function format(value: string | number): string {
if (typeof value === 'string') return value.trim();
return value.toFixed(2);
}
Branded Types / Nominal Types
런타임에는 같은 타입이지만 컴파일 타임에 구분.
// Brand 타입 정의
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type Email = Brand<string, 'Email'>;
// 생성 함수 (검증 포함)
function createUserId(id: string): UserId {
if (!id.startsWith('usr_')) throw new Error('Invalid user ID');
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes('@')) throw new Error('Invalid email');
return email as Email;
}
// 사용: 서로 다른 ID 타입을 혼동할 수 없음
function getUser(id: UserId): Promise<User> { ... }
function getOrder(id: OrderId): Promise<Order> { ... }
const userId = createUserId('usr_123');
const orderId = 'ord_456' as OrderId;
getUser(userId); // OK
getUser(orderId); // Compile Error! OrderId는 UserId가 아님
Zod + TypeScript 스키마 검증
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['user', 'admin', 'moderator']),
});
type User = z.infer<typeof userSchema>; // 자동 타입 추론
// 런타임 검증
const result = userSchema.safeParse(input);
if (result.success) {
console.log(result.data.name); // User 타입
} else {
console.error(result.error.flatten());
}
tsconfig.json 권장 설정
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalProperties": true,
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"paths": { "@/*": ["./src/*"] }
}
}
Anti-Patterns
1. any 남용
// Bad: any는 타입 체크를 무력화
function process(data: any) {
return data.foo.bar; // 런타임 에러 가능
}
// Good: unknown + 타입 가드
function process(data: unknown) {
if (isValidData(data)) {
return data.foo.bar; // 타입 안전
}
throw new Error('Invalid data');
}
2. Type Assertion 남용
// Bad: as로 강제 캐스팅
const user = response.data as User; // 실제로 User가 아닐 수 있음
// Good: 런타임 검증
const user = userSchema.parse(response.data); // Zod 검증
3. 불필요한 제네릭
// Bad: 의미 없는 제네릭
function getName<T extends { name: string }>(obj: T): string {
return obj.name;
}
// Good: 단순하게
function getName(obj: { name: string }): string {
return obj.name;
}
4. enum 대신 as const
// Avoid: enum은 JS 런타임 코드 생성
enum Direction { Up, Down, Left, Right }
// Prefer: as const는 타입만 생성 (tree-shaking 유리)
const Direction = {
Up: 'up',
Down: 'down',
Left: 'left',
Right: 'right',
} as const;
type Direction = (typeof Direction)[keyof typeof Direction];
// 'up' | 'down' | 'left' | 'right'
Sources
- TypeScript 5.x Handbook: typescriptlang.org/docs
- TypeScript Release Notes
- Total TypeScript — Matt Pocock
- Zod Documentation
- Effect-TS (타입 안전 함수형 프로그래밍)