본문으로 건너뛰기
Previous
Next
TypeScript Type Narrowing Complete Guide | Unions

TypeScript Type Narrowing Complete Guide | Unions

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

  1. Union: A | B (either)
  2. Intersection: A & B (all of)
  3. Literal: exact value types
  4. Type alias: reusable names
  5. 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


자주 묻는 질문 (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, Type Narrowing, Union Types, Type Guards, Literal Types, Discriminated Union 등으로 검색하시면 이 글이 도움이 됩니다.