TypeScript는 컴파일 타임에 타입을 검사하지만, 실제 데이터는 런타임에 온다. 외부 API 응답, 이벤트 핸들러, 사용자 입력 — 이것들이 unknown이나 유니온 타입으로 들어올 때 TypeScript는 “이 값이 뭔지 확인하고 써라"고 강제한다. 그 확인하는 방법이 타입 가드다.
타입 가드 4가지
1. typeof — 원시 타입 분기
function process(value: string | number) {
if (typeof value === 'string') {
value.toUpperCase() // 여기서 value는 string
} else {
value.toFixed(2) // 여기서 value는 number
}
}
typeof는 string, number, boolean, function, object 등 원시 타입에서만 쓴다.
2. instanceof — 클래스 인스턴스 분기
class NetworkError extends Error {
statusCode: number
constructor(message: string, statusCode: number) {
super(message)
this.statusCode = statusCode
}
}
function handleError(err: Error) {
if (err instanceof NetworkError) {
console.log(err.statusCode) // NetworkError로 좁혀짐
} else {
console.log(err.message)
}
}
3. in — 프로퍼티 존재 여부로 분기
interface Cat { meow(): void }
interface Dog { bark(): void }
function makeSound(animal: Cat | Dog) {
if ('meow' in animal) {
animal.meow() // Cat
} else {
animal.bark() // Dog
}
}
4. 사용자 정의 타입 가드 — is 반환 타입
가장 강력하다. 직접 타입 가드 함수를 만들어서 unknown 데이터를 안전하게 처리한다.
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value
)
}
// API 응답 검증
const data: unknown = await fetchJson('/api/user')
if (isUser(data)) {
console.log(data.name) // 타입 안전하게 User로 사용
}
value is User가 반환 타입이다. 함수가 true를 반환하면 TypeScript가 그 블록 안에서 value를 User 타입으로 좁힌다.
Discriminated Union — 가장 강력한 상태 분기 패턴
공통 리터럴 필드(status, type 등)로 유니온을 구분하는 패턴이다. 상태 관리, API 응답 처리에서 자주 쓰인다.
결제 상태 처리
type PaymentState =
| { status: 'idle' }
| { status: 'pending' }
| { status: 'success'; approvalNo: string; amount: number }
| { status: 'fail'; errorCode: string; message: string }
function renderPayment(state: PaymentState) {
switch (state.status) {
case 'idle':
return '결제 전'
case 'pending':
return '처리 중...'
case 'success':
return `승인번호: ${state.approvalNo}` // approvalNo 타입 보장
case 'fail':
return `오류: ${state.message}` // message 타입 보장
}
}
status === 'success'인 브랜치에서는 approvalNo와 amount가 보장된다. status === 'fail'에서는 errorCode와 message가 보장된다. 각 상태마다 필요한 데이터를 타입 수준에서 강제한다.
API 응답 처리
type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: string; code: number }
async function getUser(id: number): Promise<ApiResult<User>> {
try {
const user = await fetchUser(id)
return { success: true, data: user }
} catch {
return { success: false, error: '조회 실패', code: 404 }
}
}
const result = await getUser(1)
if (result.success) {
console.log(result.data.name) // User 타입 보장
} else {
console.log(result.error) // string 보장
}
result.success가 true면 data가 있고, false면 error와 code가 있다. try/catch 없이도 타입 수준에서 에러 케이스를 처리할 수 있다.
실무 패턴 — null 안전 처리
// 옵셔널 체이닝 (?.)
const city = user?.address?.city // user나 address가 null이면 undefined
// Nullish 병합 (??)
const name = user?.name ?? '이름 없음' // null/undefined이면 기본값
// null 단언 (!) — 확실할 때만
const name = user!.name // ⚠️ 실제로 null이면 런타임 오류
enum vs const 객체 vs 유니온
// enum — 런타임에 JS 객체로 남음 (번들에 포함)
enum Direction { UP = 'UP', DOWN = 'DOWN' }
// const 객체 — 값+타입 동시 활용, tree-shaking 가능
const Direction = { UP: 'UP', DOWN: 'DOWN' } as const
type Direction = typeof Direction[keyof typeof Direction] // 'UP' | 'DOWN'
// 유니온 — 가장 단순, 컴파일 후 사라짐
type Direction = 'UP' | 'DOWN'
단순한 상수값이면 유니온, 값을 변수처럼 참조해야 하면 const 객체를 쓴다. enum은 런타임에 JS 객체로 남아 번들에 포함되므로 신중하게 선택한다.
tsconfig strict 모드
타입 가드가 빛을 발하려면 strict: true가 필수다.
{
"compilerOptions": {
"strict": true, // null 체크, any 추론 금지 등 전체 활성화
"noImplicitAny": true, // any로 추론되는 것 오류 처리
"strictNullChecks": true, // null/undefined를 별도 타입으로 처리
"noUnusedLocals": true // 안 쓰는 변수 오류
}
}
strictNullChecks가 꺼져 있으면 null이 어디서든 들어갈 수 있어서 타입 가드의 효과가 절반으로 줄어든다.
마치며
타입 가드는 “컴파일러를 설득하는 방법"이다. TypeScript가 모르는 타입 정보를 런타임 조건으로 증명하면, 컴파일러가 그 블록 안에서 안전하게 타입을 좁혀준다.
이번 시리즈에서 TypeScript의 핵심을 Java와 비교하며 살펴봤다. 타입 시스템, interface/type, 제네릭, 유틸리티 타입, 타입 가드 — 이 다섯 가지를 이해하면 풀스택 프로젝트에서 Java와 TypeScript를 함께 다루는 데 충분한 기반이 된다.