유니온 타입 string | number를 받는 함수에서 toUpperCase를 호출하면 오류가 납니다. number에는 없는 메서드이기 때문입니다. 값이 실제로 string일 때만 호출하도록 타입의 범위를 좁히는 것이 Narrowing입니다. TypeScript는 조건문 분기에서 타입을 자동으로 좁혀줍니다.
JavaScript의 typeof 연산자를 이용한 가장 기본적인 방법입니다.
function format(value: string | number): string {
if (typeof value === "string") {
// 이 블록 안에서 value는 string
return value.toUpperCase();
}
// 여기서 value는 number
return value.toFixed(2);
}
typeof가 반환하는 값: "string", "number", "boolean", "object", "function", "undefined", "symbol", "bigint"
타입 가드(Type Guard): 특정 조건이 참일 때 타입을 좁혀주는 코드 패턴. TypeScript 컴파일러가 이 조건을 분석해 타입을 좁힙니다.
null이나 undefined 여부를 확인합니다.
function printLength(value: string | null | undefined) {
if (value) {
// value는 string (null, undefined, "" 제외)
console.log(value.length);
}
}
단, 빈 문자열 ""도 falsy이므로 value !== null && value !== undefined가 더 정확한 경우가 있습니다.
===로 특정 값과 비교해 타입을 좁힙니다.
type Status = "loading" | "success" | "error";
function handleStatus(status: Status) {
if (status === "error") {
// status는 "error"
console.log("오류 발생");
return;
}
// status는 "loading" | "success"
console.log(status);
}
객체에 특정 프로퍼티가 있는지 확인합니다.
type Dog = { bark(): void };
type Cat = { meow(): void };
function makeSound(animal: Dog | Cat) {
if ("bark" in animal) {
animal.bark(); // Dog로 좁혀짐
} else {
animal.meow(); // Cat으로 좁혀짐
}
}
클래스 인스턴스를 확인합니다.
function processDate(value: Date | string) {
if (value instanceof Date) {
return value.toISOString(); // Date의 메서드 사용 가능
}
return new Date(value).toISOString();
}
공통 리터럴 프로퍼티로 유니온 타입을 구별하는 패턴입니다. switch와 함께 쓰면 모든 경우를 빠짐없이 처리할 수 있습니다.
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string };
type ErrorState = { status: "error"; message: string };
type RequestState = LoadingState | SuccessState | ErrorState;
function render(state: RequestState) {
switch (state.status) {
case "loading":
return "로딩 중...";
case "success":
return state.data; // SuccessState로 확정
case "error":
return `오류: ${state.message}`; // ErrorState로 확정
}
}
never를 활용해 모든 케이스를 처리했는지 컴파일 타임에 확인할 수 있습니다.
function render(state: RequestState) {
switch (state.status) {
case "loading": return "로딩 중...";
case "success": return state.data;
case "error": return state.message;
default:
const _exhaustive: never = state; // 모든 케이스를 처리했으면 never
return _exhaustive;
}
}
나중에 RequestState에 새 케이스를 추가하면 default에서 컴파일 오류가 발생해 누락을 알려줍니다.
커스텀 함수를 타입 가드로 만드는 방법입니다. 반환 타입에 value is Type 형식을 씁니다.
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
function isDog(animal: Dog | Cat): animal is Dog {
return "bark" in animal;
}
function makeSound(animal: Dog | Cat) {
if (isDog(animal)) {
animal.bark(); // Dog로 좁혀짐
} else {
animal.meow(); // Cat으로 좁혀짐
}
}
animal is Dog가 반환 타입이면, 이 함수가 true를 반환하는 분기에서 TypeScript는 animal을 Dog로 취급합니다.
string | number
│
├── if (typeof value === "string")
│ └── string ──── 안전하게 string 메서드 사용
│
└── else
└── number ──── 안전하게 number 메서드 사용
TypeScript는 if, switch, 삼항 연산자, &&, || 등 다양한 제어 흐름을 분석해 타입을 좁힙니다. Narrowing을 잘 활용하면 as로 강제 캐스팅하거나 ! (non-null assertion)를 남발하지 않아도 안전하게 코드를 작성할 수 있습니다.