
타입스크립트에서 제네릭(Generic)이란, 특정 타입에 의존하지 않는 유연한 코드를 작성할 수 있도록 도와주는 문법이다.
쉽게 말해, 제네릭은 타입을 위한 변수라고 생각하면 된다. 함수나 클래스, 인터페이스 등을 작성할 때 고정된 타입 대신 제네릭을 사용하면, 사용하는 쪽에서 타입을 지정할 수 있게 되어 코드의 재사용성과 유연성이 극대화된다.
function identity<T>(value: T): T {
return value;
}
// 호출 시 타입을 지정
identity<string>("Hello"); // T는 string
identity<number>(123); // T는 number
위의 예시 코드를 살펴보면 T는 제네릭 타입으로, 아무 이름이나 사용 가능하다.
즉, T 대신 Type, MyType, 아무거나 같은 이름도 다 사용할 수 있다. 그러나 일반적으로는 T(Type의 약자)를 사용하는 것이 관례이다.
T (Type): 일반적인 타입K (Key), V (Value): 키-값 구조를 다룰 때E (Element): 배열 요소를 다룰 때타입스크립트에서 타입을 명시적으로 어노테이션해도 되는데, 굳이 제네릭을 쓰는 이유는 더 유연하면서도 안전한 코드를 작성하기 위함이다.
직접 타입을 어노테이션하면 고정된 타입에 대해서만 동작하지만,
제네릭은 다양한 타입에 대해 동작하는 코드를 하나의 구현으로 처리할 수 있다.
직접 어노테이션 하는 방식
function identityString(value: string): string {
return value;
}
function identityNumber(value: number): number {
return value;
}
// 매번 타입마다 새로운 함수를 작성해야 함
identityString("Hello");
identityNumber(123);
위의 경우 함수의 동작은 같지만, 타입마다 별도의 함수를 정의해야 해서 중복 코드가 늘어남을 알 수 있다.
제네릭 사용
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
// 배열 타입에 따라 T가 자동으로 추론됨
getFirstElement<number>([1, 2, 3]); // T는 number
getFirstElement<string>(["a", "b", "c"]); // T는 string
제네릭을 사용하면 배열 타입에 따라 자동으로 T가 추론 된다. 따라서 하나의 함수로 다양한 타입을 처리할 수 있다.
getFirstElement 뒤에 명시적으로 <number>나 <string>을 적어주지 않아도 자동으로 추론되지만 안정성을 위해 작성해주는게 좋다.
제네릭을 사용하면 함수, 클래스, 인터페이스 등을 재사용 가능하게 설계할 수 있다.
직접 어노테이션 하는 방식
interface StringKeyValuePair {
key: string;
value: string;
}
interface NumberKeyValuePair {
key: number;
value: number;
}
위 경우에는 특정 타입만 처리하는 인터페이스를 여러 번 정의해야 하며, 타입이 추가될수록 중복 코드가 늘어날 것이다.
제네릭 사용
interface KeyValuePair<K, V> {
key: K;
value: V;
}
const stringPair: KeyValuePair<string, string> = { key: "id", value: "123" };
const numberPair: KeyValuePair<number, number> = { key: 1, value: 100 };
제네릭을 통해 하나의 인터페이스로 다양한 타입을 처리하는 것을 확인할 수 있다.
고정된 타입만 처리하는 함수나 클래스는 직접 어노테이션이 더 간단하고 적합할 수 있지만, 다양한 타입에 대해 동작해야 하는 코드에는 제네릭이 더 유리하다.
제네릭은 직접 어노테이션만으로는 해결할 수 없는 유연성과 안정성을 제공하며, 이를 통해 코드의 재사용성과 가독성 또한 크게 향상시킬 수 있으므로 잘 익혀두면 타입스크립트를 더 효율적으로 활용하는 데 큰 도움이 될 것 같다.