제네릭은 C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징입니다. 특히, 한가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용됩니다.
제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미
function getText<T>(text: T): T {
return text;
}
위 함수는 제네릭 기본 문법이 적용된 형태입니다. 이제 함수를 호출할 때 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있다.
getText<string>('hi');
getText<number>(10);
getText<boolean>(true);
function logText(text: string): string {
return text;
}
이 함수의 인자와 반환 값은 모두 string으로 지정되어 있지만 만약 여러 가지 타입을 허용하고 싶다면 아래와 같이 any
를 사용할 수 있습니다
function logText(text: any): any {
return text;
}
이렇게 타입을 바꾼다고 해서 함수의 동작에 문제가 생기진 않습니다. 다만, 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지는 알 수가 없습니다. 왜냐하면 any
라는 타입은 타입 검사를 하지 않기 때문입니다.
이러한 문제점을 해결할 수 있는 것이 제네릭입니다. 제네릭은 다음과 같이 사용합니다.
function logText<T>(text: T): T {
return text;
}
먼저 함수의 이름 바로 뒤에 라는 코드를 추가했습니다. 그리고 함수의 인자와 반환 값에 모두 T 라는 타입을 추가합니다. 이렇게 되면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 됩니다. 따라서, 함수의 입력 값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있게 됩니다.
그리고 이렇게 선언한 함수는 아래와 같이 2가지 방법으로 호출할 수 있다.
// #1
const text = logText<string>("Hello Generic");
// #2
const text = logText("Hello Generic");
class Stack<T> {
private data: T[] = [];
constructor() {}
push(item: T): void {
this.data.push(item);
}
pop(): T {
return this.data.pop();
}
}
생성자를 호출하여 객체를 만들 때 T로 사용될 타입을 지정해주기만 하면 된다.
const numberStack = new Stack<number>();
const stringStack = new Stack<string>();
numberStack.push(1);
stringStack.push('a');
이제 각 스택은 항상 생성할 때 선언한 타입만을 저장하고 리턴한다. 이렇게 하면 컴파일러가 리턴하는 타입을 알 수 있게 되므로 에디터에서 자동 완성을 사용할 수 있게 되므로 생산성 향상에도 기여한다는 장점이 있다.
function first<T>(arr: T[]): T {
return arr[0];
}
함수를 호출할 때 제네릭 문법으로 타입을 정해주기만 하면 된다.
first<number>([1, 2, 3]); // 1
제네릭 함수나 클래스에서는 두 개 이상의 타입 변수도 사용할 수 있다. 다음과 같이 두 가지 변수를 받아 쌍으로 만들어 반환하는 함수를 구현해야 한다고 가정하자.
function toPair<T, U>(a: T, b: U): [ T, U ] {
return [ a, b ];
}
제네릭을 사용하면 위와 같은 형태로 구현할 수 있다. 꺽쇠 안에 T와 U 두 가지의 타입 변수가 보일 것이다. 아까 관용적으로 T를 사용한다고 말했는데, 그 뒤로는 알파벳 순서대로 사용하면 된다. 반복문에서 관용적으로 인덱스 변수로 i, j를 사용하는 것과 비슷하다.
toPair<string, number>('1', 1); // [ '1', 1 ]
toPair<number, number>(1, 1); // [ 1, 1 ]