6.2 타입 가드
타입 가드란?
타입스크립트는 정적 분석으로 타입을 추론하지만, 런타임에 실제 값의 종류를 확인해야 할 때가 있습니다. **타입 가드(Type Guard)**는 런타임 검사를 통해 특정 스코프 안에서 타입을 좁혀주는(narrow) 기법입니다.
타입 가드를 사용하면:
- 유니온 타입에서 구체적인 타입을 꺼낼 수 있습니다.
unknown타입 값을 안전하게 사용할 수 있습니다.- 외부 API 응답 데이터를 타입 안전하게 파싱할 수 있습니다.
타입 가드 = 런타임 검사 + 컴파일 타임 타입 좁히기
핵심 개념
1. typeof 타입 가드
typeof 연산자는 원시 타입(string, number, boolean, bigint, symbol, undefined, function)을 확인하는 데 사용합니다.
function processInput(input: string | number | boolean): string {
if (typeof input === 'string') {
// 이 블록에서 input은 string
return input.toUpperCase();
}
if (typeof input === 'number') {
// 이 블록에서 input은 number
return input.toFixed(2);
}
// 이 블록에서 input은 boolean
return input ? '참' : '거짓';
}
// typeof의 한계: 'object'는 null, 배열, 일반 객체 모두 포함
function checkObject(value: unknown): void {
if (typeof value === 'object') {
// value는 object | null — null 체크 필요!
if (value !== null) {
console.log(Object.keys(value));
}
}
}
2. instanceof 타입 가드
instanceof 연산자는 클래스(생성자 함수)의 인스턴스인지 확인합니다.
class Dog {
bark(): void { console.log('멍멍!'); }
}
class Cat {
meow(): void { console.log('야옹!'); }
}
function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark(); // Dog 타입으로 좁혀짐
} else {
animal.meow(); // Cat 타입으로 좁혀짐
}
}
// 에러 처리에서 자주 사용
function handleError(error: unknown): string {
if (error instanceof Error) {
return error.message; // Error 타입의 message 접근 가능
}
if (error instanceof TypeError) {
return `타입 오류: ${error.message}`;
}
return String(error);
}
3. in 연산자 타입 가드
in 연산자는 객체에 특정 프로퍼티가 존재하는지 확인합니다.
interface Fish {
swim(): void;
fins: number;
}
interface Bird {
fly(): void;
wings: number;
}
function move(animal: Fish | Bird): void {
if ('swim' in animal) {
// Fish 타입으로 좁혀짐
animal.swim();
} else {
// Bird 타입으로 좁혀짐
animal.fly();
}
}
// 옵셔널 프로퍼티와 함께
interface BasicUser {
id: number;
name: string;
}
interface AdminUser {
id: number;
name: string;
adminLevel: number;
permissions: string[];
}
function getPermissions(user: BasicUser | AdminUser): string[] {
if ('permissions' in user) {
return user.permissions; // AdminUser
}
return []; // BasicUser
}
4. 동등성(Equality) 타입 가드
===, !==, ==, != 비교로도 타입을 좁힐 수 있습니다.
type Direction = 'north' | 'south' | 'east' | 'west';
type ExtendedDirection = Direction | 'up' | 'down';
function isHorizontal(dir: ExtendedDirection): boolean {
if (dir === 'north' || dir === 'south') {
return false; // dir은 'north' | 'south'
}
if (dir === 'up' || dir === 'down') {
return false; // dir은 'up' | 'down'
}
return true; // dir은 'east' | 'west'
}
// null/undefined 동등성 가드
function processValue(value: string | null | undefined): string {
if (value == null) {
// null과 undefined 둘 다 걸러짐 (== 사용)
return '값 없음';
}
return value.trim(); // string으로 좁혀짐
}
5. 사용자 정의 타입 가드 (User-Defined Type Guards)
value is Type 형태의 반환 타입을 사용하여 커스텀 타입 가드를 정의합니다.
interface Cat {
kind: 'cat';
meow(): void;
}
interface Dog {
kind: 'dog';
bark(): void;
}
type Pet = Cat | Dog;
// 사용자 정의 타입 가드 — 반환 타입이 'value is Cat'
function isCat(pet: Pet): pet is Cat {
return pet.kind === 'cat';
}
function interact(pet: Pet): void {
if (isCat(pet)) {
pet.meow(); // Cat으로 좁혀짐
} else {
pet.bark(); // Dog으로 좁혀짐
}
}
// unknown 타입 검증
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
6. Assertion Functions
asserts value is Type 형태로, 조건 불만족 시 예외를 던지는 함수입니다. 함수 호출 이후의 코드에서 타입이 좁혀집니다.
function assert(condition: boolean, message: string): asserts condition {
if (!condition) throw new Error(message);
}
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new TypeError(`문자열이 아닙니다: ${typeof value}`);
}
}
function assertIsDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('값이 null 또는 undefined입니다.');
}
}
// 사용
function processConfig(config: unknown): void {
assertIsString(config); // 이 줄 이후 config는 string
console.log(config.toUpperCase()); // 안전하게 사용 가능
}
const maybeUser: User | null = getUser();
assertIsDefined(maybeUser); // 이후 maybeUser는 User
console.log(maybeUser.name); // null 체크 없이 사용 가능
코드 예제
예제 1: 복합 타입 가드 조합
interface Rectangle {
width: number;
height: number;
}
interface Circle {
radius: number;
}
interface Triangle {
base: number;
height: number;
}
type Shape = Rectangle | Circle | Triangle;
function isRectangle(shape: Shape): shape is Rectangle {
return 'width' in shape && 'height' in shape;
}
function isCircle(shape: Shape): shape is Circle {
return 'radius' in shape;
}
function isTriangle(shape: Shape): shape is Triangle {
return 'base' in shape && 'height' in shape;
}
function getArea(shape: Shape): number {
if (isCircle(shape)) return Math.PI * shape.radius ** 2;
if (isRectangle(shape)) return shape.width * shape.height;
if (isTriangle(shape)) return (shape.base * shape.height) / 2;
// 모든 케이스 처리됨 — TypeScript가 never로 추론
const _: never = shape;
return 0;
}
예제 2: 배열 타입 가드
// 배열 요소 타입 확인
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string');
}
function isNumberArray(value: unknown): value is number[] {
return Array.isArray(value) && value.every(item => typeof item === 'number');
}
// 제네릭 배열 타입 가드 팩토리
function isArrayOf<T>(
guard: (item: unknown) => item is T
): (value: unknown) => value is T[] {
return (value): value is T[] =>
Array.isArray(value) && value.every(guard);
}
// 사용
const isUserArray = isArrayOf(
(item): item is User =>
isObject(item) &&
typeof (item as any).id === 'number' &&
typeof (item as any).name === 'string'
);
const data: unknown = [{ id: 1, name: '홍길동', email: 'hong@example.com' }];
if (isUserArray(data)) {
data.forEach(user => console.log(user.name)); // 안전하게 사용
}
실전 예제
실전 1: API 응답 파싱 타입 가드
interface ApiSuccess<T> {
status: 'success';
data: T;
timestamp: string;
}
interface ApiError {
status: 'error';
message: string;
code: number;
}
type ApiResponse<T> = ApiSuccess<T> | ApiError;
// 사용자 정의 타입 가드
function isApiSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
return response.status === 'success';
}
function isApiError<T>(response: ApiResponse<T>): response is ApiError {
return response.status === 'error';
}
// 구조 검증 타입 가드
function isValidUser(value: unknown): value is User {
if (!isObject(value)) return false;
const obj = value as Record<string, unknown>;
return (
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string' &&
obj.email.includes('@')
);
}
// API 클라이언트
async function fetchUser(id: number): Promise<User> {
const raw = await fetch(`/api/users/${id}`).then(r => r.json());
// unknown으로 받아서 타입 가드로 검증
const response = raw as ApiResponse<unknown>;
if (isApiError(response)) {
throw new Error(`API 오류 ${response.code}: ${response.message}`);
}
if (!isValidUser(response.data)) {
throw new TypeError('API가 올바르지 않은 사용자 데이터를 반환했습니다.');
}
return response.data; // User 타입으로 좁혀짐
}
// 배치 처리
async function fetchUsers(ids: number[]): Promise<User[]> {
const results = await Promise.allSettled(ids.map(fetchUser));
return results
.filter((r): r is PromiseFulfilledResult<User> => r.status === 'fulfilled')
.map(r => r.value);
}
실전 2: 폼 입력 검증 시스템
// 검증 결과 타입
interface ValidationSuccess {
valid: true;
value: string;
}
interface ValidationFailure {
valid: false;
errors: string[];
}
type ValidationResult = ValidationSuccess | ValidationFailure;
// 검증 함수 타입
type Validator = (value: string) => string | null; // null = 통과, string = 오류 메시지
// 검증기 팩토리
const validators = {
required: (label: string): Validator =>
(value) => value.trim() ? null : `${label}을(를) 입력해 주세요.`,
minLength: (min: number): Validator =>
(value) => value.length >= min ? null : `최소 ${min}자 이상 입력해 주세요.`,
maxLength: (max: number): Validator =>
(value) => value.length <= max ? null : `최대 ${max}자까지 입력 가능합니다.`,
email: (): Validator =>
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : '올바른 이메일 형식이 아닙니다.',
pattern: (regex: RegExp, message: string): Validator =>
(value) => regex.test(value) ? null : message,
};
function validate(value: string, rules: Validator[]): ValidationResult {
const errors = rules
.map(rule => rule(value))
.filter((error): error is string => error !== null); // 타입 가드 사용!
if (errors.length === 0) {
return { valid: true, value: value.trim() };
}
return { valid: false, errors };
}
// 타입 가드로 결과 확인
function isValid(result: ValidationResult): result is ValidationSuccess {
return result.valid;
}
// 폼 처리
interface LoginForm {
email: string;
password: string;
}
function processLoginForm(form: LoginForm): void {
const emailResult = validate(form.email, [
validators.required('이메일'),
validators.email(),
]);
const passwordResult = validate(form.password, [
validators.required('비밀번호'),
validators.minLength(8),
validators.maxLength(100),
]);
if (!isValid(emailResult)) {
console.error('이메일 오류:', emailResult.errors);
return;
}
if (!isValid(passwordResult)) {
console.error('비밀번호 오류:', passwordResult.errors);
return;
}
// 두 결과 모두 ValidationSuccess로 좁혀짐
console.log(`로그인 시도: ${emailResult.value}`);
}
실전 3: 플러그인 시스템
// 플러그인 인터페이스 계층
interface BasePlugin {
name: string;
version: string;
}
interface StoragePlugin extends BasePlugin {
type: 'storage';
read(key: string): Promise<string | null>;
write(key: string, value: string): Promise<void>;
}
interface AuthPlugin extends BasePlugin {
type: 'auth';
login(credentials: { username: string; password: string }): Promise<string>;
logout(token: string): Promise<void>;
verify(token: string): Promise<boolean>;
}
interface LogPlugin extends BasePlugin {
type: 'log';
log(level: 'info' | 'warn' | 'error', message: string): void;
getHistory(): string[];
}
type Plugin = StoragePlugin | AuthPlugin | LogPlugin;
// 타입 가드
function isStoragePlugin(plugin: Plugin): plugin is StoragePlugin {
return plugin.type === 'storage';
}
function isAuthPlugin(plugin: Plugin): plugin is AuthPlugin {
return plugin.type === 'auth';
}
function isLogPlugin(plugin: Plugin): plugin is LogPlugin {
return plugin.type === 'log';
}
// 플러그인 레지스트리
class PluginRegistry {
private plugins: Map<string, Plugin> = new Map();
register(plugin: Plugin): void {
this.plugins.set(plugin.name, plugin);
}
getStorage(): StoragePlugin | undefined {
for (const plugin of this.plugins.values()) {
if (isStoragePlugin(plugin)) return plugin;
}
return undefined;
}
getAuth(): AuthPlugin | undefined {
for (const plugin of this.plugins.values()) {
if (isAuthPlugin(plugin)) return plugin;
}
return undefined;
}
getLogger(): LogPlugin | undefined {
for (const plugin of this.plugins.values()) {
if (isLogPlugin(plugin)) return plugin;
}
return undefined;
}
async initialize(): Promise<void> {
const logger = this.getLogger();
for (const [name, plugin] of this.plugins) {
logger?.log('info', `플러그인 초기화: ${name} v${plugin.version}`);
}
}
}
고수 팁
팁 1: isNonNullable 패턴
// null과 undefined를 동시에 걸러내는 타입 가드
function isNonNullable<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
// 사용: 배열에서 null 제거
const maybeUsers: (User | null | undefined)[] = [
{ id: 1, name: '홍길동', email: 'hong@test.com' },
null,
{ id: 2, name: '이순신', email: 'lee@test.com' },
undefined,
];
// filter의 타입이 자동으로 User[]로 추론됨
const users: User[] = maybeUsers.filter(isNonNullable);
팁 2: 타입 가드 함수 재사용 — 제네릭 활용
// 특정 키와 값 타입으로 객체를 검증하는 제네릭 가드
function hasProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, unknown> {
return isObject(obj) && key in (obj as object);
}
function hasPropertyOfType<K extends string, V>(
obj: unknown,
key: K,
guard: (v: unknown) => v is V
): obj is Record<K, V> {
return hasProperty(obj, key) && guard((obj as any)[key]);
}
// 사용
function isProduct(value: unknown): value is { id: number; price: number; name: string } {
return (
hasPropertyOfType(value, 'id', isNumber) &&
hasPropertyOfType(value, 'price', isNumber) &&
hasPropertyOfType(value, 'name', isString)
);
}
팁 3: Assertion Functions와 Option 타입
// Option/Maybe 패턴과 assertion 결합
type Option<T> = T | null | undefined;
function assertSome<T>(
option: Option<T>,
message = '값이 존재하지 않습니다.'
): asserts option is T {
if (option === null || option === undefined) {
throw new Error(message);
}
}
function assertType<T>(
value: unknown,
guard: (v: unknown) => v is T,
message = '타입이 올바르지 않습니다.'
): asserts value is T {
if (!guard(value)) {
throw new TypeError(message);
}
}
// 사용 예시
function processEnvVar(name: string): string {
const value = process.env[name];
assertSome(value, `환경 변수 ${name}이(가) 설정되지 않았습니다.`);
return value; // string으로 좁혀짐
}
팁 4: 타입 가드와 필터 결합
// Promise.allSettled 결과 처리
async function settledResults<T>(promises: Promise<T>[]): Promise<{
fulfilled: T[];
rejected: Error[];
}> {
const results = await Promise.allSettled(promises);
const fulfilled = results
.filter((r): r is PromiseFulfilledResult<T> => r.status === 'fulfilled')
.map(r => r.value);
const rejected = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map(r => r.reason instanceof Error ? r.reason : new Error(String(r.reason)));
return { fulfilled, rejected };
}
정리 표
| 타입 가드 | 문법 | 대상 타입 | 주의사항 |
|---|---|---|---|
| typeof | typeof x === 'string' | 원시 타입 | typeof null === 'object' 주의 |
| instanceof | x instanceof Dog | 클래스 인스턴스 | 프로토타입 체인 기반, 직렬화 후 깨짐 |
| in | 'prop' in x | 객체 프로퍼티 | null/undefined에 적용 불가 |
| 동등성 | x === 'value' | 리터럴 타입 | == null 체크 관용구 |
| 사용자 정의 | value is Type 반환 | 모든 타입 | 본문 로직의 정확성은 개발자 책임 |
| Assertion | asserts value is Type | 모든 타입 | 실패 시 반드시 throw 필요 |
| isNonNullable | value is T (null/undefined 제외) | nullable 타입 | 배열 filter와 자주 조합 |
다음 장에서는 **매핑 타입(6.3)**을 살펴봅니다. { [K in keyof T]: ... } 문법으로 기존 타입을 변환하는 강력한 기법과, Partial, Readonly, Pick 같은 유틸리티 타입을 직접 구현하는 방법을 학습합니다.