제네릭 (Generic)
TypeScript의 제네릭은 함수, 클래스, 인터페이스 등에 타입을 매개변수처럼 전달할 수 있는 기능이다. 즉, 한 번의 정의로 다양한 타입을 유연하게 처리할 수 있다.
타입을 위한 변수
를 만들어 놓고 함수나 클래스가 실제로 사용될 때 그 변수에 타입을 전달하는 개념이다.
제네릭의 장단점
구분 | 설명 |
---|---|
장점 | - 코드 재사용성 증가 - 타입 안정성과 유연성 확보 - 명시적인 타입 표현 가능 |
단점 | - 문법이 처음엔 어렵게 느껴질 수 있음 - 복잡한 타입 구조 시 가독성 저하 - 대규모 프로젝트에서 컴파일 시간 증가 가능 |
1. 타입 변수 (Type Variable)
타입 자리에 들어올 타입을 대신 표현하는 타입 변수를 정의할 수 있다. 제네릭 함수나 클래스 정의 시 지정되고, 실제 사용 시 구체적인 타입으로 대체된다.
ts
function genericFunction<T>(arg: T): T {
return arg;
}
interface GenericInterface<T> {
value: T;
}
class GenericClass<T> {
constructor(public value: T) {}
}
- 일반적으로
T
(Type),U
,K
,V
등의 식별자를 사용한다. - T는 관습적으로 Type의 약자다.
2. 제네릭을 사용하지 않은 코드
제네릭이 없다면 같은 구조의 함수를 타입별로 중복 작성해야 한다.
ts
// 터입별 배열
let numbers: number[] = [1, 2, 3, 4, 5];
let strings: string[] = ["a", "b", "c"];
// number 전용 함수
function getFirstElement(arr: number[]) {
if (!arr.length) {
return undefined;
}
return arr[0];
}
// string 전용 함수
function gerFirstStringElement(arr: string[]) {
if (!arr.length) {
return undefined;
}
return arr[0];
}
// 각각의 타입에 맞는 함수 호출
const firstNumber = getFirstElement(numbers);
const firstString = gerFirstStringElement(strings);
3. 제네릭 함수로 통합
하나의 제네릭 함수로 위의 중복된 코드를 통합할 수 있다.
ts
// 제네릭을 사용한 함수
function getFirstElement<T>(arr: T[]): T | undefined {
if (!arr.length) {
return undefined;
}
return arr[0];
}
// number와 string 배열
let numbers: number[] = [1, 2, 3, 4, 5];
let strings: string[] = ["a", "b", "c"];
// 타입 추론에 의해 자동으로 타입 결정됨
const firstNumber = getFirstElement(numbers); // number | undefined
const firstString = getFirstElement(strings); // string | undefined
- 하나의 함수로 여러 타입 배열을 처리할 수 있다. TypeScript가 전달된 인수 타입을 보고 자동으로 추론한다.
4. DOM API에서의 제네릭 활용
document.querySelector()
나 useRef()
같은 브라우저/React API도 내부적으로 제네릭을 활용한다.
ts
const div = document.querySelector<HTMLDivElement>("#myDiv");
const button = document.querySelector<HTMLButtonElement>("#myButton");
button?.click(); // 타입이 보장되어 안전하게 접근 가능
querySelector<HTMLButtonElement>
처럼 요소 타입을 직접 명시하면 선택된 요소가 올바른 타입으로 인식되어.click()
,.value
등의 속성을 안전하게 사용할 수 있다.
5. 제네릭 인터페이스
ts
// 문자열 타입 딕셔너리
interface strDict {
[key: string]: string;
}
let strObj: strDict = {
name: "Binyard",
};
// 숫자 타입 딕셔너리
interface numDict {
[key: number]: number;
}
let numObj: numDict = {
age: 30,
};
ts
interface Dict<T> {
[key: string]: T;
}
let strObj: Dict<string> = {
name: "Binyard",
};
let numObj: Dict<number> = {
age: 30,
};
Dict<T>
를 정의하면key
는 문자열이지만value
는 어떤 타입(T)으로든 유연하게 대응 가능하다.
6. 제네릭 인터페이스 (다중 타입 매개변수)
제네릭은 여러 개의 타입 매개변수를 동시에 가질 수도 있다.
ts
interface Entry<K, V> {
key: K;
value: V;
}
let entry1: Entry<string, number> = {
key: "age",
value: 30,
};
let entry2: Entry<number, string[]> = {
key: 1,
value: ["red", "green", "blue"],
};
- 이런 형태는 실제로
Map<K, V>
,Record<K, V>
등에서 자주 사용된다.
7. 제네릭 클래스 예제
ts
// 제네릭 클래스
class Item<T> {
#content: T | null;
constructor() {
this.#content = null;
}
// T 타입의 값만 설정 가능
setItem(value: T) {
this.#content = value;
}
// 저장된 값을 반환
getItem(): T | null {
return this.#content;
}
}
// number 타입을 저장하는 인스턴스
const numberItem = new Item<number>();
numberItem.setItem(100);
console.log(numberItem.getItem()); // 100
// string 타입을 저장하는 인스턴스
const stringItem = new Item<string>();
stringItem.setItem("Hello");
console.log(stringItem.getItem()); // Hello
8. 제네릭 인터페이스 + 클래스 구현
ts
// 공통 모델
interface User {
id: number;
name: string;
}
interface Product {
id: number;
name: string;
price: number;
}
// 공통 저장소 인터페이스
interface Store<T> {
findById(id: number): T | undefined;
save(item: T): void;
}
// User 저장소
class UserRepository implements Store<User> {
#users: User[] = []; // private 필드
// 사용자 ID로 조회
findById(id: number): User | undefined {
return this.#users.find((user) => user.id === id);
}
// 사용자 추가
save(user: User): void {
this.#users.push(user);
}
}
// Product 저장소
class ProductRepository implements Store<Product> {
#products: Product[] = [];
findById(id: number): Product | undefined {
return this.#products.find((product) => product.id === id);
}
save(product: Product): void {
this.#products.push(product);
}
}
// 인스턴스 생성
const userRepo = new UserRepository();
const productRepo = new ProductRepository();
userRepo.save({
id: 1,
name: "Bin",
});
userRepo.save({
id: 2,
name: "Milou",
});
productRepo.save({
id: 20,
price: 200,
name: "Mouse",
});
console.log(userRepo.findById(1)); // { id: 1, name: 'Bin' }
console.log(userRepo.findById(3)); // undefined
console.log(productRepo.findById(20)); // { id: 20, price: 200, name: "Mouse" }
9. 제네릭에 제약 추가 extends
특정 속성을 반드시 포함해야 하는 타입만 받도록 제약을 걸 수 있다.
ts
interface WithId {
id: number;
}
interface Store<T extends WithId> {
findById(id: number): T | undefined;
save(item: T): void;
}
T extends WithId
로 지정하면, id 속성을 가진 타입만Store<T>
에 전달할 수 있다.- 즉,
id
가 없는 객체를 저장하려고 하면 컴파일 단계에서 에러가 발생한다.