TypeScript Generics 활용 패턴: 타입 안전한 재사용 가능한 코드
TypeScript Generics의 다양한 활용 패턴과 실용적인 사용 사례를 설명합니다.
TypeScript Generics 활용 패턴
Introduction
TypeScript Generics는 타입을 매개변수로 전달하여 재사용 가능한 타입 안전한 코드를 작성할 수 있게 해주는 강력한 기능입니다. 함수, 클래스, 인터페이스 등에서 다양한 타입으로 동작하는 범용적인 코드를 구현할 수 있습니다. 이 글에서는 Generics의 기본 개념부터 고급 활용 패턴까지 다루겠습니다.
Environment
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": false
}
}npx tsc --version
# 5.3.3Problem
Generics 없이 작성된 코드의 한계:
// any 타입 사용 - 타입 안전성 없음
function identity(value: any): any {
return value;
}
const result = identity('hello');
// result의 타입이 any가 되어 타입 정보 유실
// 구체 타입별 함수 중복
function getFirstNumber(arr: number[]): number | undefined {
return arr[0];
}
function getFirstString(arr: string[]): string | undefined {
return arr[0];
}
function getFirstBoolean(arr: boolean[]): boolean | undefined {
return arr[0];
}
// 동일한 로직을 타입별로 반복 작성// 타입 단언에 의존하는 코드
function parseUser(data: unknown) {
const user = data as { id: number; name: string };
return user.name.toUpperCase();
// 런타임 에러 위험
}Analysis
Generics의 타입 추론 메커니즘을 분석했습니다:
// 기본 Generics 동작
function identity(value: T): T {
return value;
}
const num = identity(42); // 타입 추론: number
const str = identity('hello'); // 타입 추론: string
// 제약 조건 (Constraints)
interface HasLength {
length: number;
}
function logLength(value: T): T {
console.log(value.length);
return value;
}
logLength('hello'); // OK: string은 length 속성 있음
logLength([1, 2, 3]); // OK: array는 length 속성 있음
logLength(42); // Error: number는 length 속성 없음 // keyof 활용
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Joel', age: 34 };
const name = getProperty(user, 'name'); // 타입: string
const age = getProperty(user, 'age'); // 타입: number Solution
1단계: 기본 Generics 패턴
// 함수 Generics
function wrapInArray(value: T): T[] {
return [value];
}
function merge(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
// 기본 타입 매개변수
function createDefault(value?: T): T {
return value ?? ({} as T);
}
// 사용 예
const nums = wrapInArray(42); // number[]
const merged = merge({ a: 1 }, { b: 2 }); // { a: number; b: number } 2단계: 인터페이스와 클래스 Generics
// 제네릭 인터페이스
interface Repository {
findById(id: string): Promise;
findAll(): Promise;
create(item: Omit): Promise;
update(id: string, item: Partial): Promise;
delete(id: string): Promise;
}
// 제네릭 클래스 구현
interface User {
id: string;
name: string;
email: string;
}
class UserRepository implements Repository {
private users: Map = new Map();
async findById(id: string): Promise {
return this.users.get(id) ?? null;
}
async findAll(): Promise {
return Array.from(this.users.values());
}
async create(userData: Omit): Promise {
const id = crypto.randomUUID();
const user: User = { ...userData, id };
this.users.set(id, user);
return user;
}
async update(id: string, updates: Partial): Promise {
const existing = this.users.get(id);
if (!existing) throw new Error('User not found');
const updated = { ...existing, ...updates };
this.users.set(id, updated);
return updated;
}
async delete(id: string): Promise {
return this.users.delete(id);
}
} 3단계: 고급 Generics 패턴
// 조건부 타입 (Conditional Types)
type IsString = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// 매핑된 타입 (Mapped Types)
type Readonly = {
readonly [K in keyof T]: T[K];
};
type Partial = {
[K in keyof T]?: T[K];
};
// 템플릿 리터럴 타입
type EventName = `on${Capitalize}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type ChangeEvent = EventName<'change'>; // 'onChange'
// 타입 안전한 이벤트 시스템
type EventMap = {
click: { x: number; y: number };
change: { value: string };
submit: { formData: FormData };
};
function on(
event: K,
handler: (payload: EventMap[K]) => void
): () => void {
// 이벤트 리스너 등록
console.log(`Registered handler for ${event}`);
return () => {
console.log(`Unregistered handler for ${event}`);
};
}
// 타입 안전한 사용
on('click', (payload) => {
console.log(payload.x, payload.y); // OK
});
on('change', (payload) => {
console.log(payload.value); // OK
console.log(payload.x); // Error: 'x'는 'change' 이벤트에 없음
}); 4단계: infer 키워드 활용
// infer를 사용한 타입 추출
type ReturnOf = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: 'Joel' };
}
type UserType = ReturnOf; // { id: number; name: string }
// 함수 매개변수 타입 추출
type FirstParam = T extends (first: infer P, ...args: any[]) => any ? P : never;
type NameType = FirstParam<(name: string, age: number) => void>; // string
// 프로미스 언래핑
type Awaited = T extends Promise ? Awaited : T;
type Result = Awaited>>; // string Lessons Learned
- 타입 추론 활용: TypeScript는 Generics 매개변수를 자동으로 추론하므로 명시적 타입 지정이 불필요한 경우가 많습니다
- 제약 조건 설계:
extends키워드로 제약 조건을 설정하여 타입 안전성을 유지하면서 범용성을 확보해야 합니다 - Mapped Types 활용: 기존 타입을 변환하는 Mapped Types로 반복적인 타입 정의를 줄일 수 있습니다
- 조건부 타입: 조건부 타입으로 복잡한 타입 로직을 표현할 수 있지만, 가독성을 위해 적절한 수준으로 유지해야 합니다
- 타입 유틸리티:
Partial,Pick,Omit등의 유틸리티 타입을 적극 활용하면 타입 정의를 효율적으로 관리할 수 있습니다
이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.