Ch 03. 사용자 정의 타입 가드
typeof와 instanceof로 처리할 수 없는 상황이 있습니다. 인터페이스 타입을 구분하거나, API 응답이 특정 형태인지 확인하거나, 복잡한 조건을 함수로 추상화하고 싶을 때입니다. 이럴 때 사용자 정의 타입 가드가 필요합니다.
is 키워드 — 타입 서술어
is 키워드는 함수의 반환 타입 자리에 쓰여 "이 함수가 true를 반환하면 인자는 특정 타입이다"라고 컴파일러에 알려줍니다.
// 파일: src/guards/user-defined.tsinterface Cat { meow(): void; purr(): void;}interface Dog { bark(): void; fetch(): void;}type Animal = Cat | Dog;// 반환 타입이 "animal is Cat" — 타입 서술어function isCat(animal: Animal): animal is Cat { return "meow" in animal;}function makeSound(animal: Animal): void { if (isCat(animal)) { // 이 블록에서 animal은 Cat animal.meow(); } else { // 이 블록에서 animal은 Dog animal.bark(); }}
타입 서술어의 형식은 매개변수명 is 타입입니다. 함수가 true를 반환할 때만 컴파일러가 타입을 좁힙니다.
실전 패턴: API 응답 검증
외부 API의 응답은 unknown 타입으로 받아 검증하는 것이 안전합니다. 타입 가드를 사용하면 검증과 타입 좁히기를 동시에 처리할 수 있습니다.
// 파일: src/guards/api-validation.tsinterface User { id: number; name: string; email: string;}interface Post { id: number; title: string; content: string; authorId: number;}function isUser(value: unknown): value is User { return ( typeof value === "object" && value !== null && "id" in value && "name" in value && "email" in value && typeof (value as User).id === "number" && typeof (value as User).name === "string" && typeof (value as User).email === "string" );}function isPost(value: unknown): value is Post { return ( typeof value === "object" && value !== null && "id" in value && "title" in value && "content" in value && "authorId" in value );}async function fetchUser(id: number): Promise<User> { const response = await fetch(`/api/users/${id}`); const data: unknown = await response.json(); if (!isUser(data)) { throw new Error("유효하지 않은 응답 형식"); } // 여기서 data는 User 타입 return data;}
배열 필터링에서의 타입 가드
Array.filter의 반환 타입은 기본적으로 원래 배열 타입 그대로입니다. 타입 가드를 활용하면 필터링 후 정확한 타입을 얻을 수 있습니다.
// 파일: src/guards/array-filter.tsfunction isNotNull<T>(value: T | null | undefined): value is T { return value !== null && value !== undefined;}const items: (string | null | undefined)[] = ["a", null, "b", undefined, "c"];// 타입 가드 없이: (string | null | undefined)[]const withNulls = items.filter((x) => x !== null);// 타입 가드 사용: string[]const withoutNulls = items.filter(isNotNull);console.log(withoutNulls); // ["a", "b", "c"]
asserts 키워드 — 단언 함수
asserts 키워드는 함수가 throw하지 않고 반환되면 특정 타입임을 보장합니다. if/else 없이 에러를 던지는 방식으로 타입을 좁힐 때 유용합니다.
// 파일: src/guards/asserts.tsfunction assertIsString(value: unknown): asserts value is string { if (typeof value !== "string") { throw new Error(`string이 필요하지만 ${typeof value}가 전달됨`); }}function assertIsNonNull<T>(value: T | null | undefined): asserts value is T { if (value === null || value === undefined) { throw new Error("값이 null 또는 undefined입니다"); }}function processConfig(config: unknown) { assertIsString((config as any)?.apiKey); // 전달된 값이 string이 아니면 에러가 발생합니다 const apiKey = (config as { apiKey: string }).apiKey; console.log(`API Key: ${apiKey}`);}
asserts는 NodeJS.assert나 테스트 코드에서 자주 볼 수 있는 패턴입니다.
// 파일: src/guards/asserts-usage.tsclass Database { private connection: { query: (sql: string) => Promise<unknown[]> } | null = null; async connect(): Promise<void> { // 실제 연결 로직... this.connection = { query: async (sql) => [], }; } private assertConnected(): asserts this is this & { connection: NonNullable<Database["connection"]>; } { if (!this.connection) { throw new Error("데이터베이스에 연결되지 않았습니다"); } } async query(sql: string): Promise<unknown[]> { this.assertConnected(); // assertConnected 이후 this.connection은 null이 아님 return this.connection.query(sql); }}
is vs asserts 선택 기준
| 상황 | 사용할 것 |
|---|---|
| 분기가 필요한 경우 (true/false로 처리) | is 타입 서술어 |
| 조건 불충족 시 예외 발생 | asserts 단언 함수 |
| 배열 필터링 | is 타입 서술어 |
| 초기화 확인, 전제 조건 검사 | asserts 단언 함수 |
사용자 정의 타입 가드는 TypeScript에서 런타임과 컴파일 타임을 연결하는 다리 역할을 합니다. 타입 가드 함수 안의 로직이 실제로 올바른지는 개발자가 보장해야 합니다. TypeScript는 반환값을 믿을 뿐, 내부 구현을 검증하지 않습니다.