TypeScript에서 타입 가드(Type Guard)란 무엇이며 구현 방법을 설명해주세요.
답변 포인트
typeof, instanceof, in 연산자 및 사용자 정의 타입 가드를 생각해보세요.
정답 및 해설
빠른 요약
타입 가드는 런타임에 타입을 좁혀(narrow) 특정 타입임을 보장하는 표현식입니다. 구현 방법: 1) typeof — 원시 타입 확인 2) instanceof — 클래스 인스턴스 확인 3) in 연산자 — 프로퍼티 존재 확인 4) 사용자 정의 타입 가드: function isUser(obj: unknown): obj is User { return typeof obj === 'object' && obj !== null && 'id' in obj; }
타입 가드(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문과 함께 사용하면 누락된 케이스를 컴파일 타임에 발견할 수 있어 유지보수성이 높습니다.