iBetter Books
수정

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는 반환값을 믿을 뿐, 내부 구현을 검증하지 않습니다.