타입 가드 — typeof·instanceof·is·asserts
유니온을 좁히는 5가지 도구. unknown 데이터를 안전하게 다루기.
5편의 narrowing 을 깊게 — 유니온 타입을 좁히는 도구 5가지를 정리합니다. typeof·instanceof·in 같은 내장 도구 외에도 사용자가 만드는 타입 술어(is) 와 어서션 함수(asserts) 까지. unknown 으로 들어온 외부 데이터를 안전하게 다루는 핵심.
typeof — primitive 좁히기
function format(x: string | number): string {
if (typeof x === "string") {
return x.toUpperCase(); // x: string
}
return x.toFixed(2); // x: number
}
// typeof 결과 7가지
typeof "x" // "string"
typeof 1 // "number"
typeof true // "boolean"
typeof undef // "undefined"
typeof null // "object" ← 함정! (4편)
typeof [] // "object"
typeof {} // "object"
typeof fn // "function"
typeof Symbol() // "symbol"
typeof 1n // "bigint"
instanceof — 클래스/생성자 좁히기
try {
doStuff();
} catch (err) {
if (err instanceof TypeError) {
console.error("타입 에러:", err.message);
} else if (err instanceof RangeError) {
console.error("범위 에러");
} else if (err instanceof Error) {
console.error("기타 에러:", err.message);
} else {
throw err;
}
}
// 사용자 클래스도
class User { constructor(public name: string) {} }
class Admin extends User { constructor(name: string, public level: number) { super(name); } }
function greet(u: User | Admin) {
if (u instanceof Admin) {
console.log(`관리자 ${u.name} (lv${u.level})`);
} else {
console.log(`사용자 ${u.name}`);
}
}
in — 속성 유무로 좁히기
type Cat = { meow(): void };
type Dog = { bark(): void };
function speak(pet: Cat | Dog) {
if ("meow" in pet) {
pet.meow(); // pet: Cat
} else {
pet.bark(); // pet: Dog
}
}
// 옵션 속성 검사
type Config = { timeout?: number };
function call(c: Config) {
if ("timeout" in c && c.timeout !== undefined) {
setTimeout(..., c.timeout);
}
}
리터럴 비교 — 구분 가능 유니온 (5편 다시)
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
if (s.kind === "circle") {
return Math.PI * s.radius ** 2; // s: circle
}
return s.side ** 2; // s: square
}
가장 자주 쓰는 narrowing. kind 같은 literal 필드 한 줄 비교로 나머지 모양이 결정됨.
사용자 정의 타입 가드 — is 키워드
// 일반 boolean 반환 함수는 narrowing 효과 없음
function isUser(x: unknown): boolean {
return typeof x === "object" && x !== null && "id" in x;
}
let data: unknown;
if (isUser(data)) {
data.id; // ❌ unknown 그대로
}
// is 키워드로 "타입 술어" 명시
function isUser(x: unknown): x is { id: number; name: string } {
return typeof x === "object" && x !== null
&& "id" in x && typeof (x as any).id === "number";
}
if (isUser(data)) {
data.id; // OK — id: number
data.name; // OK — name: string
}
is 키워드 = 약속. "이 함수가 true 를 반환하면 인자는 이 타입" 이라고 TS 에게 약속합니다. 본문 구현이 약속을 안 지키면 TS 가 못 잡습니다 — 런타임 에러로 이어지므로 검사 로직을 신중하게.
asserts — 어서션 함수
// 통과하면 그 타입이 보장됨, 안 맞으면 throw
function assertString(x: unknown): asserts x is string {
if (typeof x !== "string") {
throw new Error(`expected string, got ${typeof x}`);
}
}
let v: unknown;
assertString(v);
v.toUpperCase(); // OK — 여기 도달했다면 string 보장
// 표준 라이브러리 비슷 — Node 의 assert
import assert from "node:assert/strict";
function process(x: number | undefined) {
assert(x !== undefined, "x must be defined");
x.toFixed(2); // OK
}
안전한 외부 데이터 처리 — zod 패턴
// fetch 응답을 타입 안전하게
async function loadUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const raw: unknown = await res.json();
if (!isUser(raw)) {
throw new Error("API 응답 형식 이상");
}
return raw;
}
// 실전에서는 zod·yup·io-ts 같은 라이브러리 (런타임 + 타입 동시)
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email().optional(),
});
type User = z.infer<typeof UserSchema>;
const raw = await res.json();
const user = UserSchema.parse(raw); // throw if invalid + 타입 보장
현대 표준. 외부 입력(JSON·query string·env) 은 zod 같은 스키마 라이브러리로 한 번에 검증 + 타입. 직접 is 가드 쓰는 것보다 안전하고 변경 추적도 쉽습니다.
흐름 분석의 한계 — narrowing 깨지는 경우
function fn(obj: { x?: number }) {
if (obj.x !== undefined) {
helper(); // 함수 호출 — TS 는 obj.x 가 그대로인지 모름
obj.x.toFixed(2); // ❌ obj.x 가 다시 undefined 가능성으로 되돌아감 (보수적)
}
// 로컬 변수에 담아두면 narrowing 유지
const x = obj.x;
if (x !== undefined) {
helper();
x.toFixed(2); // OK
}
}
객체 속성 narrowing 은 함수 호출이 끼면 풀립니다 — 그 함수가 객체를 바꿨을 수 있으니까요. 로컬 변수에 담아 narrow 하는 게 안전한 패턴.
한 표로 — 타입 가드 5종
| 도구 | 주 용도 |
|---|---|
| typeof | primitive (string·number 등) |
| instanceof | 클래스 인스턴스 |
| in | 속성 유무 |
| === literal | 구분 가능 유니온 (kind/type 필드) |
| x is T (술어) | 복잡한 객체 모양 검증 |
| asserts x is T | 검증 실패 시 throw, 통과 시 타입 보장 |
15편 — 비동기 + Promise<T> 다루기
Promise·async 반환 타입, fetch 결과 타이핑, catch 에서 unknown.
📚 쉽게 배우는 타입스크립트 교재
이전: 13편 매핑 타입 · 현재: 14편 (중급) · 다음 → 15편 비동기 타입 · 진행: 14/20
이전: 13편 매핑 타입 · 현재: 14편 (중급) · 다음 → 15편 비동기 타입 · 진행: 14/20