TypeScriptTypeScript 기초강의 8개 중 7번째난이도 기초

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

발행 읽는 시간 약 1 분

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

강의 소개

타입 가드로 런타임에 타입을 좁히고, 조건부 타입과 맵드 타입으로 동적인 타입을 생성하는 TypeScript 고급 기법을 배웁니다.

타입 좁히기 (Type Narrowing)#

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

TypeScript
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 가드#

TypeScript
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)#

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

TypeScript
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 접근 가능
    }
}

사용자 정의 타입 가드#

TypeScript
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로 좁혀짐
    }
}

조건부 타입#

TypeScript
// 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)#

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

TypeScript
// 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 = "...";  // ❌

템플릿 리터럴 타입#

TypeScript
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";      // ✅

실전: 이벤트 시스템 타입#

TypeScript
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 클라이언트를 만듭니다.

Follow · Ask · Work

이 글이 도움이 됐다면

짧은 작업 기록과 데모는 인스타그램에 올립니다. 만들고 싶은 것이 있다면 프로젝트 의뢰나 협업 제안도 편하게 보내 주세요.

글쓴이

파란여우

직접 만든 AI 앱·제품의 개발·배포를 공개하는 실전형 테크 크리에이터

파란여우 소개 Instagram @bluefox._.hi

이어서 보기

만들고 싶은 것이 있다면 프로젝트 의뢰로 알려 주세요.