타입 좁히기 (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 클라이언트를 만듭니다.