[2026] TypeScript Type Narrowing Complete Guide | Unions, Guards & Discriminated 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. 아래 코드는 typescript를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Syntax: Type1 | Type2 | Type3
let value: string | number;
value = "text"; // ✅
value = 123; // ✅
// value = true; // ❌ error
Practical examples
다음은 typescript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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: 다음은 typescript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 함수 정의 및 구현
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: 아래 코드는 typescript를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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. 다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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. 다음은 typescript를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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. 다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
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
아래 코드는 typescript를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
function processInput(input: string | number) {
if (typeof input === "string") {
return input.toUpperCase();
} else {
return input.toFixed(2);
}
}
instanceof guard
다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 typescript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
아래 코드는 typescript를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 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
아래 코드는 typescript를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 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