레슨 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 가능성을 강제로 처리하게 됩니다.