TypeScript에서 타입 가드(Type Guard)란 무엇이며 구현 방법을 설명해주세요.
힌트
typeof, instanceof, in 연산자 및 사용자 정의 타입 가드를 생각해보세요.
정답 및 해설
TypeScript에서 타입 가드(Type Guard)란 무엇이며 구현 방법을 설명해주세요.
타입 가드(Type Guard)는 런타임에 특정 타입임을 확인하여 TypeScript 컴파일러가 해당 범위 내에서 타입을 좁혀(narrow) 주는 표현식 또는 함수입니다. 유니온 타입처럼 여러 타입이 가능한 상황에서 안전하게 특정 타입의 속성과 메서드에 접근할 수 있게 해줍니다. 타입 가드를 사용하면 as 캐스팅 없이도 타입 안전성을 유지하면서 유연한 코드를 작성할 수 있습니다.
타입 가드가 필요한 이유
TypeScript에서 유니온 타입을 다룰 때 각 타입 고유의 속성에 접근하려면 어떤 타입인지 먼저 확인해야 합니다. 타입 가드 없이는 컴파일 에러가 발생합니다.
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function makeSound(animal: Cat | Dog) {
// 에러: 'meow'는 Dog에 존재하지 않을 수 있음
// animal.meow();
// 타입 가드로 안전하게 접근
if ('meow' in animal) {
animal.meow(); // 여기서는 Cat으로 좁혀짐
} else {
animal.bark(); // 여기서는 Dog으로 좁혀짐
}
}
구현 방법 1: typeof 연산자
typeof는 원시 타입(string, number, boolean, symbol, bigint, undefined, function)을 구분할 때 사용합니다.
function formatValue(value: string | number): string {
if (typeof value === 'string') {
// 이 블록에서 value는 string 타입
return value.toUpperCase();
} else {
// 이 블록에서 value는 number 타입
return value.toFixed(2);
}
}
console.log(formatValue('hello')); // "HELLO"
console.log(formatValue(3.14159)); // "3.14"
function processInput(input: string | number | boolean) {
if (typeof input === 'string') {
console.log('문자열 길이:', input.length);
} else if (typeof input === 'number') {
console.log('숫자의 제곱:', input * input);
} else {
console.log('불리언 값:', input ? '참' : '거짓');
}
}
typeof의 한계
typeof null은 'object'를 반환하므로 객체 타입 구분에는 적합하지 않습니다.
typeof null === 'object'; // true (JavaScript의 역사적 버그)
typeof [] === 'object'; // true (배열도 object로 반환됨)
구현 방법 2: instanceof 연산자
instanceof는 클래스 인스턴스를 구분할 때 사용합니다. 프로토타입 체인을 확인하여 해당 클래스의 인스턴스인지 검사합니다.
class HttpError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
this.name = 'HttpError';
}
}
class ValidationError extends Error {
constructor(
public field: string,
message: string
) {
super(message);
this.name = 'ValidationError';
}
}
function handleError(error: HttpError | ValidationError | Error) {
if (error instanceof HttpError) {
// HttpError 타입으로 좁혀짐
console.log(`HTTP ${error.statusCode}: ${error.message}`);
} else if (error instanceof ValidationError) {
// ValidationError 타입으로 좁혀짐
console.log(`검증 오류 - 필드 "${error.field}": ${error.message}`);
} else {
console.log(`일반 오류: ${error.message}`);
}
}
class Rectangle {
constructor(public width: number, public height: number) {}
area() { return this.width * this.height; }
}
class Circle {
constructor(public radius: number) {}
area() { return Math.PI * this.radius ** 2; }
}
function describeShape(shape: Rectangle | Circle) {
if (shape instanceof Rectangle) {
console.log(`사각형: ${shape.width} x ${shape.height}`);
} else {
console.log(`원: 반지름 ${shape.radius}`);
}
console.log(`넓이: ${shape.area().toFixed(2)}`);
}
구현 방법 3: in 연산자
in 연산자는 객체에 특정 프로퍼티가 존재하는지 확인합니다. 인터페이스처럼 클래스가 아닌 타입을 구분할 때 유용합니다.
interface Admin {
role: 'admin';
permissions: string[];
}
interface User {
role: 'user';
email: string;
}
function getAccess(person: Admin | User) {
if ('permissions' in person) {
// Admin 타입으로 좁혀짐
console.log('권한 목록:', person.permissions.join(', '));
} else {
// User 타입으로 좁혀짐
console.log('이메일:', person.email);
}
}
interface Bird {
fly: () => void;
layEggs: () => void;
}
interface Fish {
swim: () => void;
layEggs: () => void;
}
function move(pet: Bird | Fish) {
if ('fly' in pet) {
pet.fly();
} else {
pet.swim();
}
}
구현 방법 4: 사용자 정의 타입 가드 (Type Predicate)
함수명(매개변수): 매개변수 is 타입 형식의 반환 타입을 사용해 타입 가드 함수를 직접 정의합니다.
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
// 사용자 정의 타입 가드 함수
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}
function isProduct(obj: unknown): obj is Product {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'title' in obj &&
'price' in obj
);
}
function processApiResponse(data: unknown) {
if (isUser(data)) {
console.log(`사용자: ${data.name} (${data.email})`);
} else if (isProduct(data)) {
console.log(`상품: ${data.title} - ${data.price}원`);
} else {
console.log('알 수 없는 데이터 형식');
}
}
API 응답 검증에 활용
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
function isSuccessResponse<T>(
response: ApiResponse<T>
): response is ApiResponse<T> & { data: T } {
return response.success === true && response.data !== undefined;
}
async function fetchUser(id: number) {
const response: ApiResponse<User> = await fetch(`/api/users/${id}`)
.then(res => res.json());
if (isSuccessResponse(response)) {
// response.data가 User 타입으로 좁혀짐
console.log(response.data.name);
} else {
console.error(response.error);
}
}
구현 방법 5: Discriminated Union (판별 유니온)
공통 리터럴 타입 프로퍼티를 활용해 자동으로 타입을 좁히는 패턴입니다.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'rectangle'; width: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
case 'rectangle':
return shape.width * shape.height;
}
}
type NetworkState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string }
| { status: 'error'; message: string };
function renderUI(state: NetworkState) {
switch (state.status) {
case 'idle':
return '대기 중';
case 'loading':
return '로딩 중...';
case 'success':
return `결과: ${state.data}`;
case 'error':
return `에러: ${state.message}`;
}
}
구현 방법 6: Assertion Functions
TypeScript 3.7+에서 도입된 asserts 키워드로 조건이 거짓이면 예외를 던지는 타입 가드입니다.
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new TypeError(`Expected string, got ${typeof value}`);
}
}
function processName(name: unknown) {
assertIsString(name);
// 이 시점부터 name은 string 타입
console.log(name.toUpperCase());
}
실전 활용 예시: 폼 유효성 검사
interface LoginForm {
username: string;
password: string;
}
interface RegisterForm extends LoginForm {
email: string;
confirmPassword: string;
}
function isRegisterForm(form: LoginForm | RegisterForm): form is RegisterForm {
return 'email' in form && 'confirmPassword' in form;
}
function handleFormSubmit(form: LoginForm | RegisterForm) {
if (isRegisterForm(form)) {
if (form.password !== form.confirmPassword) {
throw new Error('비밀번호가 일치하지 않습니다.');
}
console.log(`회원가입: ${form.username} (${form.email})`);
} else {
console.log(`로그인: ${form.username}`);
}
}
방법별 비교 요약
| 방법 | 대상 타입 | 특징 | 사용 시기 |
|---|---|---|---|
typeof | 원시 타입 | 내장 연산자, 간단 | string, number, boolean 구분 |
instanceof | 클래스 인스턴스 | 프로토타입 체인 확인 | 클래스 기반 타입 구분 |
in | 객체 프로퍼티 | 인터페이스에 유용 | 특정 속성 존재 여부 확인 |
| 사용자 정의 | 모든 타입 | 복잡한 검증 가능 | API 응답, 복잡한 타입 검증 |
| Discriminated Union | 유니온 타입 | 컴파일 타임 완전성 검사 | 상태 머신, 명확한 타입 구분 |
| Assertion Functions | 모든 타입 | 예외 발생 방식 | 전제 조건 검사 |
핵심 정리
- 타입 가드는
as캐스팅의 안전한 대안으로, 런타임 검증과 컴파일 타임 타입 안전성을 동시에 제공합니다. - 단순 원시 타입은
typeof, 클래스는instanceof, 인터페이스는in또는 사용자 정의 타입 가드를 사용합니다. - Discriminated Union 패턴은
switch문과 함께 사용하면 누락된 케이스를 컴파일 타임에 발견할 수 있어 유지보수성이 높습니다.