
타입스크립트를 처음 배울 때 Generic이라는 개념을 마주치면 '이건 대체 왜 있는 걸까?' 하는 생각을 한 번쯤 해보셨을 겁니다. 저도 그랬으니까요! 하지만 제네릭은 타입스크립트를 강력하게 만들어주는 핵심 기능 중 하나입니다.
타입스크립트 공식 문서에서는 제네릭을 이렇게 설명합니다.
"단일 타입이 아닌 다양한 타입에서 작동하는 재사용 가능한 컴포넌트를 만들기 위해 도입되었습니다."
쉽게 말해, 타입을 함수의 파라미터처럼 사용하는 기능이죠. 이를 통해 우리는 유연하면서도 타입 안정성을 잃지 않는 코드를 작성할 수 있습니다.
만약 number 타입과 string 타입을 모두 지원하고 싶다면, 우리는 각 타입에 맞는 함수를 따로 만들어야 합니다.
// 숫자를 위한 identity 함수
function identityNumber(arg: number): number {
return arg;
}
// 문자열을 위한 identity 함수
function identityString(arg: string): string {
return arg;
}
기능은 완전히 똑같은데, 단지 타입이 다르다는 이유만으로 함수를 계속 중복해서 만들어야 하죠. 타입이 100개라면 함수도 100개를 만들어야 할까요? 정말 비효율적입니다.
function identity<T>(arg: T): T {
return arg;
}
여기서 가 바로 제네릭을 사용한다는 표시입니다. T는 타입 변수(Type Variable)로, 실제 타입이 들어올 자리를 잠시 맡아두는 플레이스홀더입니다.
이제 identity 함수는 number를 넣으면 number를 반환하고, string을 넣으면 string을 반환하는 똑똑한 함수가 되었습니다. 코드 중복은 사라지고 타입 안정성은 그대로 유지됩니다.
타입의 제약
때로는 제네릭에 특정 조건을 걸고 싶을 때가 있습니다. "아무 타입이나 받지는 말고, 특정 프로퍼티를 가진 타입만 받았으면 좋겠어!" 와 같은 경우죠. 이것을 제네릭 제약(Generic Constraints) 이라고 합니다.
length 프로퍼티를 가진 타입만 처리하는 함수를 만들어 볼까요?
// 'length' 프로퍼티가 있어야 한다는 인터페이스 정의
interface LengthWise {
length: number;
}
// T는 LengthWise 인터페이스를 만족하는 타입만 가능!
function logLength<T extends LengthWise>(item: T): void {
console.log(`'item'의 길이는 ${item.length}입니다.`);
}
extends 키워드를 사용해서 타입 T가 반드시 LengthWise 구조를 가져야 한다고 제약을 걸었습니다. 이제 logLength 함수는 length 프로퍼티를 가진 값만 인자로 받을 수 있습니다.
logLength([1, 2, 3]); // ✅ 배열은 length가 있으니 통과!
logLength("test"); // ✅ 문자열도 length가 있으니 통과!
logLength({ length: 10 }); // ✅ length 프로퍼티가 있는 객체도 통과!
// logLength(4); // ❌ Error! 'number' 타입에는 'length' 프로퍼티가 없습니다.
class Stack<T> {
private items: T[] = [];
// T 타입의 아이템만 push 가능
push(item: T) {
this.items.push(item);
}
// T 타입의 아이템을 pop (없으면 undefined)
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
이제 이 Stack 클래스는 제네릭 덕분에 number 전용 스택, string 전용 스택, 심지어 User 객체 전용 스택으로도 변신할 수 있습니다.
const numberStack = new Stack<number>();
numberStack.push(10);
// numberStack.push("hello"); // ❌ Error!
const stringStack = new Stack<string>();
stringStack.push("world");