Learning
레슨 9 / 10·6개 토픽

조건부 타입과 선언 파일

조건부 타입 (Conditional Types)

조건부 타입은 T extends U ? X : Y 형태로 타입 수준에서 조건 분기를 수행합니다. 제네릭과 결합하면 입력 타입에 따라 다른 출력 타입을 반환하는 유연한 타입을 만들 수 있습니다.

typescript
// 기본 조건부 타입
type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>;   // true
type B = IsArray<number>;     // false

// 실전: 입력 타입에 따라 반환 타입 결정
type ApiResponse<T> = T extends "user"
  ? { id: number; name: string }
  : T extends "post"
  ? { id: number; title: string; body: string }
  : never;

type UserRes = ApiResponse<"user">;  // { id: number; name: string }
type PostRes = ApiResponse<"post">;  // { id: number; title: string; body: string }

// Distributive Conditional Types — Union에 자동 분배
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[]  (← (string | number)[]가 아님!)

infer 키워드

infer 키워드를 사용하면 조건부 타입 안에서 타입 변수를 선언하고, 패턴 매칭으로 타입을 추출할 수 있습니다.

typescript
// Promise에서 내부 타입 추출
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<number>;           // number

// 함수의 첫 번째 매개변수 타입 추출
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type P = FirstParam<(name: string, age: number) => void>;  // string

// 배열의 마지막 요소 타입 추출
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

type L = Last<[1, 2, 3]>;  // 3

// 실전: 이벤트 핸들러에서 이벤트 타입 추출
type EventFromHandler<T> = T extends (event: infer E) => void ? E : never;

type ClickEvent = EventFromHandler<(e: MouseEvent) => void>;  // MouseEvent

템플릿 리터럴 타입 활용

typescript
// 타입 수준의 문자열 조합
type Method = "get" | "post" | "put" | "delete";
type Endpoint = "/users" | "/posts";

type ApiRoute = `${Uppercase<Method>} ${Endpoint}`;
// "GET /users" | "GET /posts" | "POST /users" | "POST /posts" | ...

// 객체 키에서 getter/setter 이름 자동 생성
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface Person { name: string; age: number }

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

// 타입 수준 문자열 파싱
type ExtractParam<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
  ? Param | ExtractParam<Rest>
  : T extends `${string}:${infer Param}`
  ? Param
  : never;

type Params = ExtractParam<"/users/:id/posts/:postId">;
// "id" | "postId"

선언 파일 (.d.ts)

선언 파일(.d.ts)은 JavaScript 라이브러리에 타입 정보를 제공합니다. declare 키워드로 타입만 선언하며, 실제 구현은 포함하지 않습니다. @types/ 패키지는 DefinitelyTyped 커뮤니티에서 관리하는 타입 선언 모음입니다.

typescript
// ─── custom.d.ts — 커스텀 선언 파일 ───

// 모듈 선언 — JS 라이브러리에 타입 부여
declare module "legacy-lib" {
  export function calculate(a: number, b: number): number;
  export const VERSION: string;
}

// CSS/이미지 모듈 선언
declare module "*.css" {
  const styles: { [key: string]: string };
  export default styles;
}
declare module "*.png" {
  const src: string;
  export default src;
}

// 전역 변수 선언
declare const __DEV__: boolean;
declare const API_URL: string;

// 전역 인터페이스 확장
declare global {
  interface Window {
    analytics: {
      track(event: string, data?: Record<string, unknown>): void;
    };
  }
}

export {}; // 모듈로 인식시키기 위한 빈 export

@types 패키지와 트리플 슬래시 지시자

typescript
// @types 패키지 설치
// npm install --save-dev @types/node @types/react @types/lodash

// tsconfig.json에서 타입 지정
// {
//   "compilerOptions": {
//     "types": ["node", "jest"],         // 포함할 @types
//     "typeRoots": ["./types", "./node_modules/@types"]
//   }
// }

// ─── 트리플 슬래시 지시자 (레거시, 보통 tsconfig로 대체) ───

/// <reference types="node" />
/// <reference path="./custom-types.d.ts" />

// 파일 상단에서만 사용 가능
// types: @types 패키지 참조
// path: 특정 파일 참조

tsconfig.json 핵심 옵션 정리

json
{
  "compilerOptions": {
    // ─── 타입 체크 ───
    "strict": true,                 // 모든 strict 옵션 (권장)
    "noUncheckedIndexedAccess": true, // 인덱스 접근 시 undefined 포함
    "exactOptionalProperties": true,  // optional과 undefined 구분

    // ─── 모듈 시스템 ───
    "module": "ESNext",
    "moduleResolution": "bundler",  // Vite, Webpack 등 번들러 사용 시
    "esModuleInterop": true,        // CJS/ESM 호환
    "resolveJsonModule": true,      // import data from './data.json'
    "isolatedModules": true,        // 파일 단위 트랜스파일 보장

    // ─── 경로 ───
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],           // import from '@/utils'
      "@components/*": ["./src/components/*"]
    },

    // ─── 출력 ───
    "target": "ES2022",
    "outDir": "./dist",
    "declaration": true,            // .d.ts 파일 생성
    "declarationMap": true,         // .d.ts.map 생성 (소스 추적)
    "sourceMap": true,

    // ─── 프로젝트 설정 ───
    "skipLibCheck": true,           // node_modules 타입 체크 건너뛰기
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*", "types/**/*.d.ts"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
💡

새 프로젝트에서는 "strict": true를 기본으로 설정하세요. 개별 strict 옵션을 끄는 것보다, 모두 켜고 필요할 때만 예외 처리하는 것이 안전합니다. "noUncheckedIndexedAccess": true도 함께 켜면 배열/객체 인덱스 접근 시 undefined 가능성을 강제로 처리하게 됩니다.