TypeScriptMedium#90
타입 가드(Type Guard)란 무엇이며, 사용하는 방법들을 설명해주세요.
#TypeScript#타입가드#타입좁히기#타입시스템
힌트
typeof, instanceof, in 연산자와 사용자 정의 타입 가드를 떠올려보세요.
정답 및 해설
타입 가드(Type Guard)란 무엇이며, 사용하는 방법들을 설명해주세요.
타입 가드(Type Guard)는 런타임에 타입을 좁혀(narrow) TypeScript 컴파일러가 특정 코드 블록 내에서 더 구체적인 타입을 추론하게 하는 방법입니다. TypeScript는 유니온 타입(string | number)처럼 여러 타입이 가능한 변수를 다룰 때, 타입 가드를 통해 정확한 타입을 판별하고 타입별로 안전하게 접근할 수 있게 해줍니다.
타입 내로잉(Narrowing)이란?
function processValue(value: string | number) {
// 여기서 value는 string | number 타입
value.toUpperCase(); // TS 에러! number에는 toUpperCase가 없음
value.toFixed(2); // TS 에러! string에는 toFixed가 없음
// 타입 가드로 좁히면
if (typeof value === 'string') {
// 이 블록 안에서 value는 string 타입
value.toUpperCase(); // OK
} else {
// 이 블록 안에서 value는 number 타입
value.toFixed(2); // OK
}
}
1. typeof 타입 가드
원시 타입(primitive type) 판별에 사용합니다.
// typeof가 반환하는 값: "string", "number", "boolean", "bigint",
// "symbol", "undefined", "object", "function"
function formatValue(value: string | number | boolean | null | undefined): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
if (typeof value === 'number') {
return value.toFixed(2);
}
if (typeof value === 'boolean') {
return value ? '예' : '아니오';
}
// null과 undefined는 typeof로 완전히 구분 불가
// typeof null === 'object' (JavaScript의 오래된 버그)
if (value === null) return '(없음)';
return '(미정)'; // undefined
}
// 실제 활용
function calculateTotal(price: number, discount?: number): number {
if (typeof discount === 'undefined') {
return price;
}
return price * (1 - discount);
}
2. instanceof 타입 가드
클래스 인스턴스 타입 판별에 사용합니다.
class NetworkError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = 'ValidationError';
}
}
function handleError(error: Error) {
if (error instanceof NetworkError) {
// 이 블록 안에서 error는 NetworkError 타입
console.error(`네트워크 오류 ${error.statusCode}: ${error.message}`);
if (error.statusCode === 401) {
redirectToLogin();
}
} else if (error instanceof ValidationError) {
// 이 블록 안에서 error는 ValidationError 타입
console.error(`검증 오류 - ${error.field}: ${error.message}`);
showFieldError(error.field, error.message);
} else {
console.error(`알 수 없는 오류: ${error.message}`);
}
}
// Date, Array 등 내장 클래스에도 활용
function processInput(input: string | Date) {
if (input instanceof Date) {
return input.toISOString(); // input은 Date 타입
}
return new Date(input).toISOString(); // input은 string 타입
}
3. in 연산자 타입 가드
객체가 특정 속성을 가지고 있는지 확인합니다.
interface Dog {
name: string;
bark(): void;
}
interface Cat {
name: string;
meow(): void;
}
interface Bird {
name: string;
fly(): void;
sing(): void;
}
type Animal = Dog | Cat | Bird;
function makeSound(animal: Animal) {
if ('bark' in animal) {
// animal은 Dog 타입
animal.bark();
} else if ('meow' in animal) {
// animal은 Cat 타입
animal.meow();
} else {
// animal은 Bird 타입
animal.fly();
animal.sing();
}
}
// 선택적 속성 확인에도 활용
interface AdminUser {
name: string;
adminLevel: number;
permissions: string[];
}
interface RegularUser {
name: string;
}
type User = AdminUser | RegularUser;
function showDashboard(user: User) {
if ('adminLevel' in user) {
// user는 AdminUser 타입
console.log(`관리자 레벨: ${user.adminLevel}`);
console.log(`권한: ${user.permissions.join(', ')}`);
} else {
// user는 RegularUser 타입
console.log('일반 사용자');
}
}
4. 사용자 정의 타입 가드 (Type Predicate)
반환 타입에 arg is Type 형식을 사용하여 타입 가드 함수를 직접 만듭니다.
// 기본 문법: function fn(arg: T): arg is SpecificType
interface Cat { type: 'cat'; meow(): void }
interface Dog { type: 'dog'; bark(): void }
type Animal = Cat | Dog;
// 사용자 정의 타입 가드 함수
function isCat(animal: Animal): animal is Cat {
return animal.type === 'cat';
}
function isDog(animal: Animal): animal is Dog {
return animal.type === 'dog';
}
function handleAnimal(animal: Animal) {
if (isCat(animal)) {
animal.meow(); // animal은 Cat 타입으로 좁혀짐
} else {
animal.bark(); // animal은 Dog 타입으로 좁혀짐
}
}
// 실용적인 예: null/undefined 체크
function isNonNull<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const values: (number | null | undefined)[] = [1, null, 2, undefined, 3];
const nonNullValues = values.filter(isNonNull); // number[] 타입!
// isNonNull 없이 filter하면 (number | null | undefined)[] 타입
// API 응답 검증
interface SuccessResponse {
success: true;
data: unknown;
}
interface ErrorResponse {
success: false;
error: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function isSuccess(response: ApiResponse): response is SuccessResponse {
return response.success === true;
}
async function fetchData() {
const response: ApiResponse = await callApi();
if (isSuccess(response)) {
processData(response.data); // response.data 접근 가능
} else {
showError(response.error); // response.error 접근 가능
}
}
5. Discriminated Union (판별 유니온)
공통된 리터럴 타입 필드(태그)를 이용해 유니온 타입을 구분합니다.
// 도형 타입 - 각각 고유한 kind 값을 가짐
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// shape는 Circle 타입
return Math.PI * shape.radius ** 2;
case 'rectangle':
// shape는 Rectangle 타입
return shape.width * shape.height;
case 'triangle':
// shape는 Triangle 타입
return (shape.base * shape.height) / 2;
default:
// 모든 케이스를 처리했는지 컴파일 타임에 검증 (Exhaustiveness Check)
const _exhaustiveCheck: never = shape;
throw new Error(`처리되지 않은 shape: ${_exhaustiveCheck}`);
}
}
// Redux Action 패턴에서 활용
type AppAction =
| { type: 'INCREMENT'; amount: number }
| { type: 'DECREMENT'; amount: number }
| { type: 'RESET' }
| { type: 'SET_VALUE'; value: number };
function reducer(state: number, action: AppAction): number {
switch (action.type) {
case 'INCREMENT': return state + action.amount; // action.amount: number
case 'DECREMENT': return state - action.amount; // action.amount: number
case 'RESET': return 0;
case 'SET_VALUE': return action.value; // action.value: number
}
}
6. Nullish Check (null/undefined 체크)
interface User {
name: string;
email?: string; // optional
address: string | null;
}
function processUser(user: User) {
// optional 속성 접근 시
if (user.email !== undefined) {
// user.email은 string 타입
sendEmail(user.email.toLowerCase());
}
// null 체크
if (user.address !== null) {
// user.address는 string 타입
console.log(user.address.trim());
}
// null과 undefined 동시 체크 (nullish check)
if (user.email != null) { // != null은 null과 undefined 둘 다 체크
user.email.toLowerCase();
}
// 옵셔널 체이닝으로 간소화 (타입 가드는 아니지만 유용)
const lowerEmail = user.email?.toLowerCase(); // string | undefined
}
타입 가드 조합
// 여러 타입 가드를 조합한 복잡한 케이스
type Input = string | number | boolean | null | undefined | object;
function processInput(input: Input) {
// null/undefined 먼저 처리
if (input == null) {
return 'empty';
}
// input은 이제 string | number | boolean | object
if (typeof input === 'object') {
if (Array.isArray(input)) {
return `배열 (${input.length}개)`;
}
return `객체: ${JSON.stringify(input)}`;
}
if (typeof input === 'string') {
return input.trim();
}
if (typeof input === 'number') {
return input.toFixed(2);
}
// input은 boolean
return input ? 'true' : 'false';
}
정리
| 타입 가드 방법 | 사용 시기 | 예시 |
|---|---|---|
typeof | 원시 타입 구분 | typeof x === 'string' |
instanceof | 클래스 인스턴스 구분 | x instanceof Error |
in 연산자 | 속성 존재 여부로 구분 | 'swim' in animal |
| 사용자 정의 (Type Predicate) | 복잡한 조건으로 구분 | function isCat(a): a is Cat |
| Discriminated Union | 태그 필드로 구분 | switch(shape.kind) |
| Nullish Check | null/undefined 제거 | if (x != null) |
Array.isArray | 배열 여부 구분 | Array.isArray(x) |
핵심: 타입 가드는 TypeScript에게 "이 조건을 통과하면 이 타입이 확실하다"고 알려주는 방법입니다. 런타임 안전성과 컴파일 타임 타입 안전성을 동시에 달성합니다.