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

타입에 대한 사고방식이 강하면 자연스럽게 다음과 같은 코드를 쓰고 싶어집니다:
typeof animal === Fish
하지만 JavaScript는 그렇게 동작하지 않습니다.
1) 핵심 아이디어: 타입은 런타임에 존재하지 않는다
TypeScript 타입은 컴파일 후 사라집니다. 런타임에서는 오직 JavaScript 값만 존재합니다.
런타임 검사는 항상 다음과 같은 형태입니다:
typeof x === "string"x instanceof Date"swim" in animalanimal.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 특이점)!value는 falsy 값(0,"",false)을 검사합니다. (null/undefined만이 아니라)"prop" in obj는 프로토타입 때문에true가 될 수 있습니다.- 선택적 프로퍼티는 두 분기 모두에서 타입이 포함될 수 있습니다(예:
Human에swim?()가 있을 때).
Takeaway
- 런타임 검증은 JavaScript 체크에서 나옵니다.
- 컴파일 타임 안전성은 TypeScript narrowing에서 나옵니다.
- 재사용이 필요할 때는 type predicate로 체크를 감싸세요.
“JS 체크를 이용해 타입을 좁히고, 타입 가드로 그 좁힌 결과를 재사용한다.”