제네릭이란?
💡 제네릭은 타입스크립트에서 함수, 클래스, 인터페이스 등을 다양한 타입에서 재사용할 수 있도록 해주는 템플릿이다. 💡
즉, 코드를 작성할 때 타입을 확정하지 않고, 사용하는 시점에 원하는 타입을 지정할 수 있게 해준다.
제네릭을 사용하면 "any" 타입을 사용할 때처럼 타입 유연성을 가질 수 있으면서도, 컴파일 시점에 타입 체크를 할 수 있어 타입 안전성도 확보할 수 있다. 이것이 제네릭의 가장 큰 장점이라고 생각한다!
제네릭은 꺾쇠괄호(<>)를 사용하여 표현하며, 주로 T, E, K, V 등의 단일 대문자를 사용하는 것이 관례다.
(물론 다른 이름을 사용해도 무방하다.)
제네릭 함수
가장 먼저 살펴볼 것은 제네릭 함수다!
예를 들어, 어떤 값을 받아서 그대로 반환하는 간단한 함수를 만들어보자.
function identity<T>(arg: T): T {
return arg;
}
// 사용 예시
const num = identity<number>(5); // 명시적으로 타입 지정
const str = identity("Hello"); // 타입 추론으로 string 타입으로 결정됨
위 코드에서 identity 함수는 제네릭 T를 사용한다.
이 함수는 T 타입의 인자를 받아서 같은 T 타입을 반환한다.
함수를 호출할 때 타입을 명시적으로 지정하거나, 전달하는 인자에 따라 타입스크립트가 타입을 추론하게 할 수 있다.
이렇게 제네릭을 사용하면 다양한 타입에 대해 동작하는 함수를 타입 안전하게 만들 수 있다.
만약 제네릭 없이 구현한다면 any 타입을 사용해야 할 텐데, 그러면 타입 정보가 손실되어 타입 안전성이 떨어진다.
좀 더 실용적인 예제를 보자. 배열에서 첫 번째 요소를 반환하는 함수를 만들어보겠다.
function getFirstElement<T>(arr: T[]): T | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
// 사용 예시
const numbers = [1, 2, 3, 4, 5];
const firstNumber = getFirstElement(numbers); // 타입은 number | undefined
const names = ["Alice", "Bob", "Charlie"];
const firstName = getFirstElement(names); // 타입은 string | undefined
const mixed = [1, "two", true];
const firstMixed = getFirstElement(mixed); // 타입은 number | string | boolean | undefined
이 함수는 어떤 타입의 배열이든 받아서 그 배열의 첫 번째 요소를 반환한다.
빈 배열일 경우를 대비해 undefined도 반환 타입에 포함시켰다.
타입스크립트는 전달된 배열의 타입을 자동으로 추론하여 반환 타입을 결정해준다.
제네릭 인터페이스
함수뿐만 아니라 인터페이스에서도 제네릭을 사용할 수 있다.
이를 통해 다양한 타입에 대해 동일한 구조를 가진 인터페이스를 정의할 수 있다.
interface Box<T> {
value: T;
getValue(): T;
}
// 구현 예시
class NumberBox implements Box<number> {
constructor(public value: number) {}
getValue(): number {
return this.value;
}
}
const box = new NumberBox(42);
console.log(box.getValue()); // 42
위 코드에서 Box 인터페이스는 제네릭 타입 T를 사용한다.
이 인터페이스를 구현하는 클래스는 특정 타입을 지정해야 한다. (위 예시에서는 NumberBox가 Box<number>를 구현하고 있음)
여러 제네릭 타입 매개변수를 사용하는 것도 가능하다.
interface Pair<K, V> {
key: K;
value: V;
}
const pair1: Pair<string, number> = { key: "age", value: 25 };
const pair2: Pair<number, boolean> = { key: 1, value: true };
이처럼 제네릭 인터페이스를 사용하면 다양한 타입 조합에 대해 재사용 가능한 타입 정의를 만들 수 있다!
여기서!
만약 pari1을 { key: 123, value: 25}로 할당하려고하면 key가 string이 아니기에
Type 'number' is not assignable to type 'string'
이런 오류가 발생할 것이다!
제네릭 클래스
클래스에서도 제네릭을 사용할 수 있다.
제네릭 클래스는 다양한 타입에 대해 재사용 가능한 컴포넌트를 만들 때 유용하다.
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
peek(): T | undefined {
return this.items[0];
}
size(): number {
return this.items.length;
}
}
// 사용 예시
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
const firstItem = numberQueue.dequeue(); // 타입은 number | undefined
const stringQueue = new Queue<string>();
stringQueue.enqueue("Hello");
stringQueue.enqueue("World");
const peekedItem = stringQueue.peek(); // 타입은 string | undefined
위 코드는 제네릭을 사용한 간단한 큐 구현이다. (Queue 클래스는 어떤 타입의 아이템이든 저장할 수 있음)
클래스를 인스턴스화할 때 원하는 타입을 지정할 수 있으며, 그 후에는 지정된 타입만 큐에 추가할 수 있다!
이렇게 제네릭 클래스를 사용하면 타입 안전성을 유지하면서도 다양한 타입에 대해 동작하는 재사용 가능한 자료구조를 구현할 수 있다.
제네릭 제약조건
때로는 제네릭 타입에 특정 조건을 부여하고 싶을 때가 있다.
이럴 때 제네릭 제약조건(Generic Constraints)을 사용할 수 있다.
제약조건은 extends 키워드를 사용하여 표현한다.
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
// 사용 예시
logLength("Hello"); // 문자열은 length 속성이 있음
logLength([1, 2, 3]); // 배열도 length 속성이 있음
logLength({ length: 10, value: 3 }); // length 속성이 있는 객체
// 오류 예시
// logLength(10); // 오류: number 타입에는 length 속성이 없음
위 코드에서 T extends HasLength는 "T는 HasLength 인터페이스를 확장(구현)해야 한다"라는 의미다.
즉, T 타입은 반드시 length 속성을 가져야 한다.
따라서 문자열, 배열, length 속성을 가진 객체 등은 이 함수의 인자로 사용할 수 있지만, length 속성이 없는 숫자 등은 사용할 수 없다.
이렇게 제약조건을 사용하면 제네릭의 유연성을 유지하면서도 특정 조건을 만족하는 타입만 사용하도록 제한할 수 있다!!
keyof 연산자와 함께 사용하는 예제도 살펴보자.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = {
name: "John",
age: 30,
isEmployed: true
};
// 사용 예시
const name = getProperty(person, "name"); // 타입은 string
const age = getProperty(person, "age"); // 타입은 number
const isEmployed = getProperty(person, "isEmployed"); // 타입은 boolean
// 오류 예시
// const height = getProperty(person, "height"); // 오류: 'height'는 'person'의 키가 아님
이 예제에서 K extends keyof T는 "K는 T 타입의 키 중 하나여야 한다"라는 의미다.
즉, getProperty 함수는 객체 obj의 키로 존재하는 프로퍼티만 접근할 수 있도록 제한된다.
이렇게 하면 존재하지 않는 프로퍼티에 접근하는 오류를 컴파일 시점에 방지할 수 있다.
개인적으로 타입스크립트를 더 깊이 있게 사용하고 싶다면 제네릭을 마스터하는 것이 큰 도움이 될거라고 생각이 든다!
이 글이 제네릭 이해에 조금이나마 도움이 되었기를 바란다.
'TypeScript' 카테고리의 다른 글
[TypeScript] 타입 별칭(Type Aliases) 개념정리 (0) | 2025.03.07 |
---|---|
[TypeScript] 열거형(Enums) 개념정리 (0) | 2025.03.05 |
[TypeScript] 인터페이스 개념정리 (0) | 2025.03.03 |
[TypeScript] JavaScript의 진화!!! (0) | 2025.03.01 |