Ch 02. 제네릭 인터페이스와 클래스
함수뿐 아니라 인터페이스와 클래스에도 타입 매개변수를 붙일 수 있습니다. 데이터를 담는 컨테이너나 공통 패턴을 가진 도구들을 재사용 가능하게 만드는 데 필수적인 기법입니다.
제네릭 인터페이스
// 파일: src/generics/generic-interface.ts// 제네릭 응답 래퍼 — API 응답에 자주 쓰는 패턴interface ApiResponse<T> { data: T; status: number; message: string; timestamp: Date;}interface User { id: number; name: string; email: string;}interface Product { id: number; title: string; price: number;}// 동일한 ApiResponse 인터페이스로 다양한 응답 표현const userResponse: ApiResponse<User> = { data: { id: 1, name: "Alice", email: "[email protected]" }, status: 200, message: "OK", timestamp: new Date(),};const productResponse: ApiResponse<Product[]> = { data: [ { id: 1, title: "TypeScript 책", price: 30000 }, { id: 2, title: "JavaScript 책", price: 25000 }, ], status: 200, message: "OK", timestamp: new Date(),};
제네릭 인터페이스로 페이지네이션 표현하기
// 파일: src/generics/pagination.tsinterface Page<T> { items: T[]; total: number; page: number; pageSize: number; hasNext: boolean; hasPrev: boolean;}interface Cursor<T> { items: T[]; nextCursor: string | null; prevCursor: string | null;}// 페이지네이션을 처리하는 제네릭 함수function paginate<T>(items: T[], page: number, pageSize: number): Page<T> { const start = (page - 1) * pageSize; const end = start + pageSize; const paginatedItems = items.slice(start, end); return { items: paginatedItems, total: items.length, page, pageSize, hasNext: end < items.length, hasPrev: page > 1, };}
Repository 패턴 — 제네릭 클래스
제네릭 클래스는 데이터 접근 계층을 추상화할 때 특히 유용합니다.
// 파일: src/generics/repository.tsinterface Entity { id: number;}class Repository<T extends Entity> { private items: Map<number, T> = new Map(); private nextId = 1; create(data: Omit<T, "id">): T { const id = this.nextId++; const item = { ...data, id } as T; this.items.set(id, item); return item; } findById(id: number): T | undefined { return this.items.get(id); } findAll(): T[] { return Array.from(this.items.values()); } update(id: number, data: Partial<Omit<T, "id">>): T | undefined { const existing = this.items.get(id); if (!existing) return undefined; const updated = { ...existing, ...data } as T; this.items.set(id, updated); return updated; } delete(id: number): boolean { return this.items.delete(id); } count(): number { return this.items.size; }}// 사용interface User extends Entity { name: string; email: string;}interface Post extends Entity { title: string; content: string; authorId: number;}const userRepo = new Repository<User>();const postRepo = new Repository<Post>();const alice = userRepo.create({ name: "Alice", email: "[email protected]" });const bob = userRepo.create({ name: "Bob", email: "[email protected]" });const post = postRepo.create({ title: "TypeScript 제네릭", content: "제네릭은 강력합니다.", authorId: alice.id,});console.log(userRepo.findById(1)); // { id: 1, name: "Alice", ... }console.log(postRepo.findAll()); // [{ id: 1, title: "TypeScript 제네릭", ... }]
제네릭 클래스의 상속
// 파일: src/generics/generic-inheritance.tsclass TimestampedRepository<T extends Entity> extends Repository<T> { private timestamps: Map<number, { created: Date; updated: Date }> = new Map(); create(data: Omit<T, "id">): T { const item = super.create(data); this.timestamps.set(item.id, { created: new Date(), updated: new Date(), }); return item; } update(id: number, data: Partial<Omit<T, "id">>): T | undefined { const item = super.update(id, data); if (item) { const ts = this.timestamps.get(id); if (ts) { ts.updated = new Date(); } } return item; } getTimestamps(id: number) { return this.timestamps.get(id); }}
제네릭으로 Stack 구현하기
// 파일: src/generics/stack.tsclass Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; } isEmpty(): boolean { return this.items.length === 0; } get size(): number { return this.items.length; } toArray(): T[] { return [...this.items]; }}// 숫자 스택const numStack = new Stack<number>();numStack.push(1);numStack.push(2);numStack.push(3);console.log(numStack.pop()); // 3// 문자열 스택const strStack = new Stack<string>();strStack.push("first");strStack.push("second");console.log(strStack.peek()); // "second"
제네릭 인터페이스로 전략 패턴 구현하기
// 파일: src/generics/strategy.tsinterface Sorter<T> { sort(items: T[]): T[]; compare(a: T, b: T): number;}class NumberSorter implements Sorter<number> { sort(items: number[]): number[] { return [...items].sort(this.compare); } compare(a: number, b: number): number { return a - b; }}class StringSorter implements Sorter<string> { sort(items: string[]): string[] { return [...items].sort(this.compare); } compare(a: string, b: string): number { return a.localeCompare(b); }}function sortAndFirst<T>(items: T[], sorter: Sorter<T>): T | undefined { const sorted = sorter.sort(items); return sorted[0];}const smallest = sortAndFirst([3, 1, 4, 1, 5], new NumberSorter()); // 1const first = sortAndFirst(["banana", "apple", "cherry"], new StringSorter()); // "apple"
제네릭 인터페이스와 클래스는 단순한 타입 재사용을 넘어서 아키텍처 수준의 추상화를 가능하게 합니다. Repository, Container, Strategy 같은 패턴을 제네릭으로 구현하면 타입 안전성을 유지하면서 여러 도메인에서 같은 구조를 재사용할 수 있습니다.