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: "alice@test.com",
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: "alice@test.com"
};
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
Related posts
- C++ union and std::variant
- TypeScript interfaces | Complete guide
- TypeScript generics | Complete guide
- C++ algorithm set operations
- C++ std::variant vs union
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. TypeScript type narrowing explained: typeof, instanceof, in, equality checks, discriminated unions, and custom type pred…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- TypeScript 시작하기 | 설치, 설정, 기본 문법
- TypeScript 인터페이스 | Interface 완벽 가이드
- JavaScript Promise & async/await Complete Guide
Keywords Covered in This Article (Related Search Terms)
This article covers TypeScript, Type Narrowing, Union Types, Type Guards, Literal Types, Discriminated Union.