SHIN
TypeScript 실전 팁 10선
10편as 없이 타입 검증과 좁은 타입 추론을 동시에 얻습니다.
type Color = 'red' | 'green' | 'blue'
type Palette = Record<string, Color | Color[]>
// ❌ as로 강제 캐스팅 — 추론 정보 손실
const palette = {
primary: 'red',
secondary: ['green', 'blue'],
} as Palette
palette.primary.toUpperCase() // ❌ Color | Color[]이라 .toUpperCase() 없음
// ✅ satisfies — 타입 검사 + 추론 유지
const palette2 = {
primary: 'red',
secondary: ['green', 'blue'],
} satisfies Palette
palette2.primary.toUpperCase() // ✅ 'red'로 좁혀짐 (string)
palette2.secondary.map(c => c) // ✅ Color[]로 좁혀짐// as const 없이 — 넓은 타입
const config = {
endpoint: '/api/v1',
methods: ['GET', 'POST'],
}
// { endpoint: string; methods: string[] }
// as const — 리터럴 타입 유지
const config2 = {
endpoint: '/api/v1',
methods: ['GET', 'POST'],
} as const
// { readonly endpoint: '/api/v1'; readonly methods: readonly ['GET', 'POST'] }
// 활용: 객체에서 유니온 타입 추출
const STATUS = {
PENDING: 'pending',
ACTIVE: 'active',
INACTIVE: 'inactive',
} as const
type Status = typeof STATUS[keyof typeof STATUS]
// 'pending' | 'active' | 'inactive'// ❌ 실수로 ID를 바꿔 넘겨도 컴파일 통과
function transfer(fromId: number, toId: number, amount: number) {}
transfer(userId, amount, toId) // 순서 바꿔도 오류 없음
// ✅ 브랜드 타입으로 구별
type UserId = number & { readonly _brand: 'UserId' }
type AccountId = number & { readonly _brand: 'AccountId' }
type Amount = number & { readonly _brand: 'Amount' }
function asUserId(id: number): UserId { return id as UserId }
function asAccountId(id: number): AccountId { return id as AccountId }
function asAmount(n: number): Amount { return n as Amount }
function transfer(from: UserId, to: UserId, amount: Amount) {}
transfer(asUserId(1), asUserId(2), asAmount(100)) // ✅
transfer(asUserId(1), asAmount(100), asUserId(2)) // ❌ 타입 불일치복잡한 타입 연산은 컴파일 시간을 늘립니다.
// ❌ 느림 — 재귀 깊이가 깊은 분산 조건부 타입
type DeepNested<T> = T extends object
? { [K in keyof T]: DeepNested<T[K]> }
: T
// ✅ 빠름 — interface 캐싱 활용
interface DeepNestedI<T extends object> {
[K in keyof T]: T[K] extends object ? DeepNestedI<T[K]> : T[K]
}// ❌ 느림 — 모든 타입을 분산시키는 유니온
type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never
// 타입 복잡도를 측정하는 방법: tsc --extendedDiagnostics
// Types: 큰 숫자면 타입 최적화 필요// any: 타입 검사 완전 비활성화 — 최후의 수단
function processAny(data: any) {
data.anything() // 컴파일 통과 — 위험
}
// unknown: 사용 전 좁히기 강제
function processUnknown(data: unknown) {
data.anything() // ❌ 컴파일 오류
if (typeof data === 'object' && data !== null) {
// 여기서 data는 object로 좁혀짐
}
}
// 제네릭: 호출 시점에 타입 결정 — 가장 좋음
function processGeneric<T>(data: T): T {
return data
}// null/undefined 제거
type NonNullable<T> = T & {} // 내장 유틸리티와 동일
// 배열 요소 타입
type ArrayElement<T> = T extends (infer U)[] ? U : never
// 객체 값 타입
type ValueOf<T> = T[keyof T]
// 특정 값 타입의 키만 추출
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T]
interface Form {
name: string
age: number
email: string
active: boolean
}
type StringFieldKeys = KeysOfType<Form, string> // 'name' | 'email'
type NumberFieldKeys = KeysOfType<Form, number> // 'age'
// 재귀 Partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}TypeScript의 타입 시스템은 단순한 문서화 도구가 아니라 버그를 예방하는 실질적인 안전망입니다. 이 10가지 팁을 프로젝트에 점진적으로 적용해보세요.