숫자 배열의 첫 번째 원소를 반환하는 함수를 만들었습니다. 나중에 문자열 배열에도 같은 함수가 필요해졌습니다. any를 쓰면 타입 안전성을 잃고, 함수를 복제하면 유지보수가 어렵습니다. 제네릭은 이 문제의 해답입니다.
// any: 타입 안전성 없음
function firstAny(arr: any[]): any {
return arr[0];
}
const result = firstAny([1, 2, 3]);
result.toUpperCase(); // 런타임 오류 — TypeScript가 number임을 모름
// 타입별 중복 함수: 코드 중복
function firstNumber(arr: number[]): number { return arr[0]; }
function firstString(arr: string[]): string { return arr[0]; }
타입 매개변수를 <T>로 선언합니다. T는 함수가 호출될 때 실제 타입으로 채워집니다.
function first<T>(arr: T[]): T {
return arr[0];
}
const n = first([1, 2, 3]); // T = number, 반환 타입 number
const s = first(["a", "b"]); // T = string, 반환 타입 string
n.toFixed(2); // 정상 — n이 number임을 알고 있음
s.toUpperCase(); // 정상 — s가 string임을 알고 있음
TypeScript가 인수에서 T를 추론하므로, 대부분의 경우 명시적으로 타입을 지정하지 않아도 됩니다.
first<number>([1, 2, 3]); // 명시 — 가능하지만 불필요
first([1, 2, 3]); // 추론 — 권장
타입 매개변수(Type Parameter): 제네릭에서 실제 타입 대신 쓰는 자리 표시자. 관례상 T(Type), K(Key), V(Value), E(Element)처럼 대문자 한 글자를 씁니다.
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const entry = pair("name", "철수"); // [string, string]
const entry2 = pair(1, true); // [number, boolean]
인터페이스에도 타입 매개변수를 사용할 수 있습니다.
interface Box<T> {
value: T;
getValue(): T;
}
const stringBox: Box<string> = {
value: "안녕",
getValue() { return this.value; },
};
const numberBox: Box<number> = {
value: 42,
getValue() { return this.value; },
};
class 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];
}
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.pop(); // number | undefined
타입 매개변수에 제약을 걸어, 특정 구조를 가진 타입만 받도록 할 수 있습니다.
// T는 length 프로퍼티가 있는 타입이어야 함
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength("안녕하세요"); // 5
getLength([1, 2, 3]); // 3
getLength(42); // 오류: number에는 length가 없습니다
객체에서 존재하는 키만 허용하도록 제약을 걸 수 있습니다.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "철수", age: 30 };
getProperty(user, "name"); // "철수" (string)
getProperty(user, "age"); // 30 (number)
getProperty(user, "email"); // 오류: "email"은 User의 키가 아닙니다
타입 매개변수에 기본값을 설정할 수 있습니다.
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
const generic: ApiResponse = { data: "뭐든지", status: 200 }; // T = unknown
const typed: ApiResponse<string> = { data: "문자열", status: 200 };
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json() as T;
}
interface User {
id: number;
name: string;
}
const user = await fetchData<User>("/api/users/1");
user.name; // string — 타입 안전하게 사용 가능
제네릭은 코드 재사용성과 타입 안전성을 동시에 잡는 도구입니다. any를 쓰고 싶은 순간이 오면 먼저 제네릭으로 해결할 수 있는지 검토해 보는 것이 좋습니다.