타입 가드 심화 — 커스텀 타입 가드 함수와 asserts
커스텀 타입 가드는
is키워드로 "이 함수가 true를 반환하면 매개변수의 타입이 좁혀진다"고 TypeScript에게 알려주는 것입니다.
타입 가드 함수 (Type Predicate)
// 반환 타입이 'value is string' — 타입 술어(Type Predicate)
function isString(value: unknown): value is string {
return typeof value === 'string';
}
const input: unknown = 'hello';
if (isString(input)) {
// input: string — 타입이 좁혀짐
console.log(input.toUpperCase());
}
일반 boolean 반환과의 차이
// ❌ 일반 boolean — 타입이 좁혀지지 않음
function isStringBool(value: unknown): boolean {
return typeof value === 'string';
}
const data: unknown = 'test';
if (isStringBool(data)) {
// data: unknown — 여전히 unknown!
// data.toUpperCase(); // Error
}
// ✅ 타입 가드 — 타입이 좁혀짐
function isStringGuard(value: unknown): value is string {
return typeof value === 'string';
}
if (isStringGuard(data)) {
// data: string — 좁혀짐!
data.toUpperCase(); // OK
}
객체 타입 가드
interface User {
type: 'user';
name: string;
email: string;
}
interface Admin {
type: 'admin';
name: string;
permissions: string[];
}
type Person = User | Admin;
function isAdmin(person: Person): person is Admin {
return person.type === 'admin';
}
function showInfo(person: Person) {
if (isAdmin(person)) {
console.log('권한:', person.permissions); // OK — Admin
} else {
console.log('이메일:', person.email); // OK — User
}
}
복잡한 검증 로직
interface ApiResponse {
data: unknown;
status: number;
}
interface UserData {
id: number;
name: string;
email: string;
}
// 런타임 검증을 타입 가드로 래핑
function isUserData(data: unknown): data is UserData {
if (typeof data !== 'object' || data === null) return false;
const obj = data as Record<string, unknown>;
return (
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string'
);
}
// 사용
async function fetchUser(): Promise<UserData | null> {
const response = await fetch('/api/user');
const data = await response.json();
if (isUserData(data)) {
return data; // UserData로 안전하게 반환
}
return null;
}
배열 필터링에서의 타입 가드
const items: (string | null | undefined)[] = ['hello', null, 'world', undefined, 'ts'];
// ❌ 일반 filter — (string | null | undefined)[]
const filtered = items.filter((item) => item != null);
// 타입이 여전히 (string | null | undefined)[]
// ✅ 타입 가드 filter — string[]
const filtered2 = items.filter((item): item is string => item != null);
// string[]
면접에서 "filter로 null을 제거했는데 타입이 안 좁혀져요"라는 문제의 해결법으로 자주 나옵니다.
Assertion Functions (asserts)
asserts 키워드를 사용하면 함수가 실패 시 예외를 던지고, 성공하면 타입을 좁히는 패턴을 구현할 수 있습니다.
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`Expected string, got ${typeof value}`);
}
}
const data: unknown = 'hello';
assertIsString(data);
// 이 줄 이후부터 data: string
console.log(data.toUpperCase()); // OK
assert 함수 만들기
// 일반적인 assert 함수
function assert(condition: unknown, message?: string): asserts condition {
if (!condition) {
throw new Error(message ?? 'Assertion failed');
}
}
const user: User | null = getUser();
assert(user !== null, '사용자를 찾을 수 없습니다');
// user: User — null이 제거됨
console.log(user.name); // OK
Node.js의 assert와의 관계
import assert from 'node:assert';
const value: string | number = getValue();
assert(typeof value === 'string');
// value: string — Node.js의 assert도 타입을 좁힘
is vs asserts
// is — if 문에서 사용, 타입을 좁히거나 좁히지 않거나
function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
if (isNumber(data)) {
// 좁혀짐
} else {
// 좁혀지지 않음
}
// asserts — 함수 호출 이후 무조건 좁혀짐 (실패하면 예외)
function assertNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') throw new Error('Not a number');
}
assertNumber(data);
// 이 줄 이후 data: number (예외 안 났으면)
| 특성 | is (Type Predicate) | asserts |
|---|---|---|
| 사용 위치 | if/else 조건 | 함수 호출 이후 |
| 실패 시 | else 분기로 이동 | 예외 발생 |
| 용도 | 조건부 타입 좁히기 | 사전 조건 검증 |
정리
- 커스텀 타입 가드는
value is Type으로 반환 타입을 지정해서 타입을 좁힌다 - 일반 boolean 반환으로는 타입이 좁혀지지 않는다
filter에서 타입 가드를 쓰면 배열의 타입이 좁혀진다asserts함수는 실패 시 예외를 던지고, 성공하면 이후 코드에서 타입을 좁힌다is는 조건부,asserts는 전제 조건 검증에 사용한다
댓글 로딩 중...