`typeof x === Fish`를 시도하지 마세요: TypeScript 타입 검증 실용 가이드 (Narrowing + Type Predicates)

발행: (2026년 1월 5일 오후 11:45 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

Cover image for Stop trying typeof x === Fish: A practical guide to TypeScript type verification (Narrowing + Type Predicates)

타입에 대한 사고방식이 강하면 자연스럽게 다음과 같은 코드를 쓰고 싶어집니다:

typeof animal === Fish

하지만 JavaScript는 그렇게 동작하지 않습니다.

1) 핵심 아이디어: 타입은 런타임에 존재하지 않는다

TypeScript 타입은 컴파일 후 사라집니다. 런타임에서는 오직 JavaScript 값만 존재합니다.
런타임 검사는 항상 다음과 같은 형태입니다:

  • typeof x === "string"
  • x instanceof Date
  • "swim" in animal
  • animal.kind === "fish"

TypeScript는 이러한 검사를 이용해 유니온 타입을 좁히는(narrowing) 작업을 수행합니다.

2) Narrowing: TypeScript가 일반적인 JS 패턴을 이해한다

typeof narrowing (원시 타입)

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") return " ".repeat(padding) + input;
  return padding + input;
}

instanceof narrowing (클래스 / 생성자)

function logValue(x: Date | string) {
  if (x instanceof Date) return x.toUTCString();
  return x.toUpperCase();
}

"in" narrowing (프로퍼티 존재 여부)

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

function move(animal: Fish | Bird) {
  if ("swim" in animal) animal.swim();
  else animal.fly();
}

Note: in 연산자는 프로토타입 체인에서도 동작하며, 선택적 프로퍼티는 두 분기 모두에 영향을 줄 수 있습니다.

3) Type predicates: narrowing을 재사용 가능하게 만들기 (진정한 장점)

여러 곳에서 같은 검사를 재사용해야 할 때(예: .filter() 내부) type predicates(사용자 정의 타입 가드)가 빛을 발합니다:

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

function isFish(animal: Fish | Bird): animal is Fish {
  return "swim" in animal;
}

function move(animal: Fish | Bird) {
  if (isFish(animal)) animal.swim();
  else animal.fly();
}

이제 같은 함수를 재사용할 수 있습니다:

const zoo: (Fish | Bird)[] = [/* ... */];
const fishes = zoo.filter(isFish); // Fish[]

구분된(discriminated) 프로퍼티를 이용한 또 다른 예시:

type ButtonAsLink = { href: string; onClick?: never };
type ButtonAsAction = { onClick: () => void; href?: never };
type Props = { label: string } & (ButtonAsLink | ButtonAsAction);

function isLinkProps(p: Props): p is Props & ButtonAsLink {
  return "href" in p;
}

function SmartButton(props: Props) {
  if (isLinkProps(props)) {
    return {props.label};
  }
  return {props.label};
}

4) 모델을 직접 제어할 수 있을 때의 베스트 프랙티스: discriminated unions

데이터 형태를 바꿀 수 있다면, 가장 견고한 방법으로 discriminated union을 사용하세요:

type Fish = { kind: "fish"; swim: () => void };
type Bird = { kind: "bird"; fly: () => void };

function move(animal: Fish | Bird) {
  if (animal.kind === "fish") animal.swim();
  else animal.fly();
}

프로퍼티 검사를 직접 하는 것보다 명확하고, 유니온이 커져도 잘 확장됩니다.

5) 흔히 빠지는 함정 (한 번만 익혀두세요)

  • typeof null === "object" (역사적인 JS 특이점)
  • !valuefalsy 값(0, "", false)을 검사합니다. (null/undefined만이 아니라)
  • "prop" in obj는 프로토타입 때문에 true가 될 수 있습니다.
  • 선택적 프로퍼티는 두 분기 모두에서 타입이 포함될 수 있습니다(예: Humanswim?()가 있을 때).

Takeaway

  • 런타임 검증은 JavaScript 체크에서 나옵니다.
  • 컴파일 타임 안전성은 TypeScript narrowing에서 나옵니다.
  • 재사용이 필요할 때는 type predicate로 체크를 감싸세요.

“JS 체크를 이용해 타입을 좁히고, 타입 가드로 그 좁힌 결과를 재사용한다.”

Back to Blog

관련 글

더 보기 »

Angular 팁 #4

소개: Angular 작업을 위한 몇 가지 팁 – 프론트엔드 개발자 관점에서 Part 4. 이 팁들은 이미 Angular 경험이 있다고 가정하므로, 우리는 …