TypeScriptTypeScript 기초 · 7기초

타입 좁히기와 고급 패턴 — 조건부 타입, 맵드 타입

TypeScript타입가드조건부타입맵드타입infer

타입 좁히기 (Type Narrowing)

유니온 타입에서 구체적인 타입을 확인합니다.

function processInput(input: string | number | null) {
    // typeof 가드
    if (typeof input === "string") {
        return input.toUpperCase();   // string
    }
    if (typeof input === "number") {
        return input.toFixed(2);      // number
    }
    return "없음";                    // null
}

instanceof 가드

class ApiError extends Error {
    constructor(public status: number, message: string) {
        super(message);
    }
}

class NetworkError extends Error {
    constructor(public retryAfter: number, message: string) {
        super(message);
    }
}

function handleError(error: ApiError | NetworkError) {
    if (error instanceof ApiError) {
        console.log(`API 오류 ${error.status}: ${error.message}`);
    } else {
        console.log(`네트워크 오류, ${error.retryAfter}초 후 재시도`);
    }
}

판별 유니온 (Discriminated Union)

공통 리터럴 타입 필드로 타입을 구분합니다.

interface Loading {
    status: "loading";
}
interface Success<T> {
    status: "success";
    data: T;
}
interface Error {
    status: "error";
    message: string;
}

type AsyncState<T> = Loading | Success<T> | Error;

function render<T>(state: AsyncState<T>): string {
    switch (state.status) {
        case "loading": return "로딩 중...";
        case "success": return JSON.stringify(state.data);  // state.data 접근 가능
        case "error":   return `오류: ${state.message}`;   // state.message 접근 가능
    }
}

사용자 정의 타입 가드

interface Cat { type: "cat"; meow(): void }
interface Dog { type: "dog"; bark(): void }
type Animal = Cat | Dog;

// 반환 타입에 "is" 키워드
function isCat(animal: Animal): animal is Cat {
    return animal.type === "cat";
}

function makeSound(animal: Animal) {
    if (isCat(animal)) {
        animal.meow();  // Cat으로 좁혀짐
    } else {
        animal.bark();  // Dog로 좁혀짐
    }
}

조건부 타입

// T가 string이면 string[], 아니면 T[]
type ArrayOf<T> = T extends string ? string[] : T[];

type StringArray = ArrayOf<string>;   // string[]
type NumberArray = ArrayOf<number>;   // number[]

// NonNullable 직접 구현
type MyNonNullable<T> = T extends null | undefined ? never : T;

type A = MyNonNullable<string | null | undefined>;  // string

// 함수 반환 타입 추출 (infer)
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type Resolved = UnpackPromise<Promise<string>>;  // string
type NotPromise = UnpackPromise<number>;          // number

맵드 타입 (Mapped Types)

기존 타입의 모든 속성을 변환합니다.

// Readonly 직접 구현
type MyReadonly<T> = {
    readonly [K in keyof T]: T[K];
};

// Optional 직접 구현
type MyPartial<T> = {
    [K in keyof T]?: T[K];
};

// 깊은 Readonly (재귀)
type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object
        ? DeepReadonly<T[K]>
        : T[K];
};

interface Config {
    server: { host: string; port: number };
    database: { url: string };
}

type FrozenConfig = DeepReadonly<Config>;
// config.server.host = "...";  // ❌

템플릿 리터럴 타입

type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

type ApiRoute = `/api/${string}`;
const route: ApiRoute = "/api/users";    // ✅
// const bad: ApiRoute = "/users";       // ❌

// CSS 속성 조합
type CSSUnit = "px" | "rem" | "em" | "%";
type CSSValue = `${number}${CSSUnit}`;
const fontSize: CSSValue = "16px";      // ✅

실전: 이벤트 시스템 타입

interface Events {
    userLogin: { userId: string; timestamp: Date };
    userLogout: { userId: string };
    itemAdded: { itemId: string; quantity: number };
}

type EventHandler<T extends keyof Events> = (data: Events[T]) => void;

class EventEmitter {
    private handlers: Partial<{
        [K in keyof Events]: EventHandler<K>[];
    }> = {};

    on<T extends keyof Events>(event: T, handler: EventHandler<T>) {
        if (!this.handlers[event]) this.handlers[event] = [];
        (this.handlers[event] as EventHandler<T>[]).push(handler);
    }

    emit<T extends keyof Events>(event: T, data: Events[T]) {
        this.handlers[event]?.forEach(h => h(data));
    }
}

const emitter = new EventEmitter();
emitter.on("userLogin", ({ userId, timestamp }) => {
    console.log(`${userId} 로그인: ${timestamp}`);
});

정리

기법설명
typeof 가드원시 타입 좁히기
instanceof 가드클래스 인스턴스 확인
판별 유니온공통 리터럴 필드로 구분
타입 가드 함수value is Type 반환
조건부 타입T extends U ? X : Y
infer조건부 타입에서 타입 추출
맵드 타입[K in keyof T]: ...
템플릿 리터럴문자열 기반 타입 조합

다음 편에서는 TypeScript 실전 프로젝트 — 배운 모든 내용을 활용한 타입 안전 API 클라이언트를 만듭니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.