TypeScript Type Narrowing Complete Guide | Unions
이 글의 핵심
TypeScript type narrowing explained: typeof, instanceof, in, equality checks, discriminated unions, and custom type predicates—write safer APIs and state machines.
Introduction
Advanced types in TypeScript let you define precise, flexible models for your data.
1. Union types
Concept
A union type means a value can be one of several types.
// Syntax: Type1 | Type2 | Type3
let value: string | number;
value = "text"; // ✅
value = 123; // ✅
// value = true; // ❌ error
Practical examples
// Function parameters
function printId(id: string | number) {
console.log(`ID: ${id}`);
}
printId(101); // ID: 101
printId("USER001"); // ID: USER001
// Arrays
let mixedArray: (string | number)[] = [1, "two", 3, "four"];
// Return type
function getResult(success: boolean): string | null {
return success ? "ok" : null;
}
Type guards
With unions you narrow types with type guards before using type-specific APIs:
// 함수 정의 및 구현
function processValue(value: string | number) {
// typeof checks at runtime; the compiler narrows the type
if (typeof value === "string") {
// ✅ In this block, value is string
console.log(value.toUpperCase());
console.log(value.length);
console.log(value.trim());
} else {
// ✅ In else, value is number
console.log(value.toFixed(2));
console.log(value * 2);
console.log(value.toExponential());
}
}
processValue("hello"); // HELLO, 5
processValue(3.14159); // 3.14, 6.28318
Without type guards:
function processValueBad(value: string | number) {
// ❌ Compile error: string | number has no shared toUpperCase
// console.log(value.toUpperCase());
// ❌ Compile error: string | number has no shared toFixed
// console.log(value.toFixed(2));
// ✅ Shared methods only
console.log(value.toString());
console.log(value.valueOf());
}
More narrowing patterns:
// 1. typeof (primitives)
function format(value: string | number | boolean) {
if (typeof value === "string") {
return value.toUpperCase();
} else if (typeof value === "number") {
return value.toFixed(2);
} else {
return value ? "true" : "false";
}
}
// 2. instanceof (class instances)
function handleError(error: Error | string) {
if (error instanceof Error) {
console.log(error.message);
console.log(error.stack);
} else {
console.log(error);
}
}
// 3. in operator (discriminate by property)
type Dog = { bark: () => void };
type Cat = { meow: () => void };
function makeSound(animal: Dog | Cat) {
if ("bark" in animal) {
animal.bark();
} else {
animal.meow();
}
}
// 4. User-defined type predicate
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase());
}
}
2. Intersection types
Concept
An intersection type must satisfy all of the combined types.
// Syntax: Type1 & Type2 & Type3
type Person = {
name: string;
age: number;
};
type Employee = {
employeeId: string;
department: string;
};
type Staff = Person & Employee;
const staff: Staff = {
name: "Alice",
age: 30,
employeeId: "E001",
department: "Engineering"
};
Practical example
// Mixin-style composition
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type User = {
id: string;
name: string;
email: string;
};
type UserWithTimestamp = User & Timestamped;
const user: UserWithTimestamp = {
id: "U001",
name: "Alice",
email: "[email protected]",
createdAt: new Date(),
updatedAt: new Date()
};
3. Literal types
Concept
A literal type pins a value to an exact constant.
// String literals
let direction: "left" | "right" | "up" | "down";
direction = "left"; // ✅
// direction = "top"; // ❌ error
// Numeric literals
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;
diceRoll = 3; // ✅
// diceRoll = 7; // ❌ error
// Boolean literal
let isTrue: true;
isTrue = true; // ✅
// isTrue = false; // ❌ error
Practical examples
// HTTP methods
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
function request(url: string, method: HttpMethod) {
console.log(`${method} ${url}`);
}
request("/api/users", "GET"); // ✅
// request("/api/users", "PATCH"); // ❌ error
// State machines
type Status = "idle" | "loading" | "success" | "error";
interface ApiState {
status: Status;
data: any;
error: string | null;
}
const state: ApiState = {
status: "loading",
data: null,
error: null
};
4. Type aliases
Concept
A type alias gives a name to a type.
// Primitives
type UserId = string;
type Age = number;
let id: UserId = "U001";
let age: Age = 25;
// Object shape
type User = {
id: UserId;
name: string;
age: Age;
email: string;
};
const user: User = {
id: "U001",
name: "Alice",
age: 25,
email: "[email protected]"
};
Function types
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const multiply: MathOperation = (a, b) => a * b;
console.log(add(10, 5)); // 15
console.log(subtract(10, 5)); // 5
5. Type narrowing
typeof guard
function processInput(input: string | number) {
if (typeof input === "string") {
return input.toUpperCase();
} else {
return input.toFixed(2);
}
}
instanceof guard
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
makeSound(new Dog()); // Woof!
makeSound(new Cat()); // Meow!
in operator
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim();
} else {
animal.fly();
}
}
User-defined type guards
interface User {
id: string;
name: string;
}
interface Admin {
id: string;
name: string;
permissions: string[];
}
function isAdmin(user: User | Admin): user is Admin {
return "permissions" in user;
}
function greet(user: User | Admin) {
if (isAdmin(user)) {
console.log(`Admin ${user.name}, permissions: ${user.permissions.join(", ")}`);
} else {
console.log(`User ${user.name}`);
}
}
6. Hands-on examples
Example 1: API response type
interface User {
id: string;
name: string;
}
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
async function fetchUser(id: string): Promise<ApiResponse<User>> {
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: "User not found" };
}
}
// Usage
const result = await fetchUser("U001");
if (result.success) {
console.log("User:", result.data.name);
} else {
console.error("Error:", result.error);
}
Example 2: State machine
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: any }
| { status: "error"; error: string };
function handleState(state: State) {
switch (state.status) {
case "idle":
console.log("Idle");
break;
case "loading":
console.log("Loading...");
break;
case "success":
console.log("Data:", state.data);
break;
case "error":
console.error("Error:", state.error);
break;
}
}
// Usage
handleState({ status: "idle" });
handleState({ status: "loading" });
handleState({ status: "success", data: { name: "Alice" } });
handleState({ status: "error", error: "Network error" });
7. Common mistakes
Mistake 1: Misusing unions
// ❌ Wrong
function getLength(value: string | number) {
return value.length; // error: number has no length
}
// ✅ Correct
function getLength(value: string | number) {
if (typeof value === "string") {
return value.length;
}
return value.toString().length;
}
Mistake 2: Conflicting intersections
// ❌ Conflicting property types
type A = { value: string };
type B = { value: number };
type C = A & B; // value becomes never
// ✅ Compatible intersection
type A = { name: string };
type B = { age: number };
type C = A & B; // { name: string; age: number }
Summary
Takeaways
- Union:
A | B(either) - Intersection:
A & B(all of) - Literal: exact value types
- Type alias: reusable names
- Narrowing: refine unions safely
Next steps
- [TypeScript interfaces](/en/blog/typescript-series-03-interface/
- [TypeScript generics](/en/blog/typescript-series-04-generics/
- TypeScript utility types
Related posts
- C++ union and std::variant
- [TypeScript interfaces | Complete guide](/en/blog/typescript-series-03-interface/
- [TypeScript generics | Complete guide](/en/blog/typescript-series-04-generics/
- C++ algorithm set operations
- C++ std::variant vs union
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. TypeScript type narrowing explained: typeof, instanceof, in, equality checks, discriminated unions, and custom type pred… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- TypeScript 시작하기 | 설치, 설정, 기본 문법
- TypeScript 인터페이스 | Interface 완벽 가이드
- [JavaScript Promise & async/await Complete Guide](/en/blog/javascript-promise-async-await-guide/
이 글에서 다루는 키워드 (관련 검색어)
TypeScript, Type Narrowing, Union Types, Type Guards, Literal Types, Discriminated Union 등으로 검색하시면 이 글이 도움이 됩니다.