어제 오늘 내일

[TypeScript] 7. 타입 좁히기(Narrowing) 본문

IT/TypeScript

[TypeScript] 7. 타입 좁히기(Narrowing)

hi.anna 2026. 8. 10. 18:34

이 글의 목표: 유니온 타입을 안전하게 다루는 모든 기법을 정리합니다. typeof, in, instanceof, 사용자 정의 타입 가드까지.

좁히기란?

넓은 타입(예: string | number)을, 코드의 특정 지점에서 더 구체적인 타입으로 "좁히는" 것입니다. TypeScript는 여러분이 작성한 조건문을 읽고, 각 분기 안에서 타입을 자동으로 좁혀줍니다.

1) typeof 가드

원시 타입을 구분할 때 씁니다.

function format(value: string | number) {
  if (typeof value === "string") {
    // 이 블록에서 value는 string
    return value.trim().toUpperCase();
  } else {
    // 이 블록에서 value는 number
    return value.toFixed(2);
  }
}

typeof로 구분 가능한 값: "string", "number", "boolean", "undefined", "object", "function", "symbol", "bigint".

2) 진리값(truthiness) 좁히기

null/undefined를 걸러낼 때 자주 씁니다.

function greet(name: string | null) {
  // name이 null이나 빈 문자열이면 아래 블록을 건너뛴다
  if (name) {
    // 여기서 name은 string (null 제거됨)
    console.log(name.toUpperCase());
  }
}

// 이른 반환(early return) 패턴이 더 읽기 좋다
function greet2(name: string | null) {
  if (!name) return; // null이면 즉시 종료
  console.log(name.toUpperCase()); // 이후로 name은 string
}

주의: if (count)0도 걸러냅니다. 숫자를 다룰 땐 if (count != null)처럼 명시적으로 쓰는 게 안전합니다.

3) 동등 비교 좁히기

type Direction = "up" | "down";
function move(dir: Direction) {
  if (dir === "up") {
    // dir은 "up"
  } else {
    // dir은 "down" (남은 하나로 좁혀진다)
  }
}

4) in 연산자 가드

객체에 특정 속성이 있는지로 구분합니다.

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  // 'swim' 속성이 있으면 Fish로 좁혀진다
  if ("swim" in animal) {
    animal.swim();
  } else {
    animal.fly();
  }
}

5) instanceof 가드

클래스 인스턴스를 구분할 때 씁니다.

function logDate(value: Date | string) {
  if (value instanceof Date) {
    // value는 Date
    console.log(value.toISOString());
  } else {
    // value는 string
    console.log(value);
  }
}

6) 판별 유니온 좁히기 (복습)

공통 태그로 좁히는 방법. 여러 속성을 가진 객체들엔 이게 가장 강력합니다.

type Success = { ok: true; data: string };
type Failure = { ok: false; error: string };

function handle(result: Success | Failure) {
  if (result.ok) {
    console.log(result.data);  // Success로 좁혀짐
  } else {
    console.log(result.error); // Failure로 좁혀짐
  }
}

7) 사용자 정의 타입 가드

복잡한 판별 로직은 함수로 빼되, 반환 타입에 value is Type 문법을 씁니다. 이걸 "타입 술어(type predicate)"라고 합니다.

type Cat = { meow: () => void };
type Dog = { bark: () => void };

// 반환 타입 'animal is Cat'이 핵심
// 이 함수가 true를 반환하면, 호출한 쪽에서 animal이 Cat으로 좁혀진다
function isCat(animal: Cat | Dog): animal is Cat {
  return "meow" in animal;
}

function speak(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow(); // ✅ Cat으로 좁혀짐
  } else {
    animal.bark(); // ✅ Dog
  }
}

boolean으로는 안 될까?

같은 함수를 반환 타입만 boolean으로 바꿔보면 차이가 분명해집니다.

// 반환 타입을 boolean으로 둔 경우 (타입 술어 아님)
function isCat(animal: Cat | Dog): boolean {
  return "meow" in animal;
}

function speak(animal: Cat | Dog) {
  if (isCat(animal)) {
    // ❌ animal은 여전히 Cat | Dog 그대로다
    animal.meow(); // 에러: Dog에는 meow가 없을 수 있다
  }
}

이유는 이렇습니다. 반환 타입이 boolean이면, TypeScript 입장에서 isCat은 그저 "참 또는 거짓을 돌려주는 함수"일 뿐입니다. 그 true/false인자의 타입에 대해 무엇을 의미하는지는 알 수 없습니다. 함수 안에서 "meow" in animal을 검사했다는 사실은 함수 내부의 일이고, 호출하는 쪽으로는 "불리언 하나"라는 정보만 전달되기 때문입니다.

반면 반환 타입을 animal is Cat으로 쓰면, 이는 단순한 불리언이 아니라 "이 함수가 true를 반환하면 인자 animalCat이다" 라는 약속을 컴파일러에게 전달하는 것입니다. 그래서 if (isCat(animal)) 블록 안에서 TypeScript가 animalCat으로 좁혀줍니다. 즉 animal is Cat은 좁히기 정보를 함수 경계 너머로 실어 나르는 "운반선" 역할을 합니다.

주의: 타입 술어를 쓰면 검사 로직의 정확성은 여러분의 책임이 됩니다. 예를 들어 return true처럼 실제와 다른 값을 돌려줘도 컴파일러는 막지 못하고, 그대로 Cat으로 믿어버립니다. 타입 술어는 "내가 이 검사를 제대로 했다"는 약속이므로, 검사 로직을 정확히 작성해야 합니다.

실전 예제: unknown 안전하게 다루기

외부에서 들어온 unknown 값을 검증할 때 타입 가드가 빛을 발합니다.

type User = { id: number; name: string };

// 임의의 값이 User 형태인지 런타임에 검사하는 타입 가드
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    typeof (value as User).id === "number" &&
    typeof (value as User).name === "string"
  );
}

function processResponse(data: unknown) {
  if (isUser(data)) {
    // 여기서 data는 안전하게 User로 취급된다
    console.log(data.name);
  } else {
    throw new Error("잘못된 사용자 데이터");
  }
}

실무에서는 이런 검증을 zod 같은 라이브러리로 자동화합니다(17편에서 다룹니다). 하지만 원리를 아는 것이 중요합니다.

정리

기법 용도
typeof 원시 타입 구분
truthiness null/undefined 제거
=== 리터럴 값 구분
in 속성 존재 여부로 객체 구분
instanceof 클래스 인스턴스 구분
판별 유니온 태그 속성으로 구분
value is T 복잡한 검증을 함수로 추출

좁히기는 TypeScript를 실전에서 쓰는 데 가장 자주 필요한 기술입니다. 다음 글에서는 함수의 다양한 타입 표현을 다룹니다.

반응형
Comments