타입스크립트는 정적 타입 언어이기 때문에 함수 또는 클래스를 정의하는 시점에 매개변수나 반환값의 타입을 선언하여야 합니다. 그런데 함수 또는 클래스를 정의하는 시점에 매개변수나 반환값의 타입을 선언하기 어려운 경우가 있습니다.
C#과 Java와 같은 정적타입 언어에서, 재사용 가능한 API나 클래스를 생성하는 기능으로 제네릭이 있습니다. 단일 타입이 아닌 다양한 타입에서 작동이 가능한 함수를 작성할 수 있습니다.
제네릭이 존재하지 않는 경우, FIFO 구조의 Queue
클래스를 생성해보겠습니다.
class Queue {
protected data: any[] = [];
push(item: any) {
this.data.push(item);
}
pop() {
return this.data.shift();
}
}
const queue = new Queue();
queue.push(0);
queue.push('1'); // 의도하지 않은 실수!
console.log(queue.pop().toFixed()); // 0
console.log(queue.pop().toFixed()); // Runtime error
Queue
클래스의 data 프로퍼티는 any[]
타입 입니다. any[]
타입은 어떤 타입의 요소도 가질 수 있는 배열을 의미하죠.
단 이러한 경우, any[]
타입은 배열 요소의 타입이 모두 같지 않다는 문제를 가지게 됩니다. 위 예제의 경우 data 프로퍼티는 number
타입만을 포함하는 배열이라는 기대 하에 각 요소에 대해 Number.prototype.toFixed
를 사용한다면, 따라서 number
타입이 아닌 요소의 경우 런타임 에러가 발생하게 됩니다.
위와 같은 문제를 해결하기 위해 Queue
클래스를 상속하여 number 타입 전용 Queue 클래스를 정의해야 합니다.
class Queue {
protected data: any[] = [];
push(item: any) {
this.data.push(item);
}
pop() {
return this.data.shift();
}
}
// Queue 클래스를 상속하여 number 타입 전용 NumberQueue 클래스를 정의
class NumberQueue extends Queue {
// number 타입의 요소만을 push한다.
push(item: number) {
super.push(item);
}
pop(): number {
return super.pop();
}
}
const queue = new NumberQueue();
queue.push(0);
// 의도하지 않은 실수를 사전 검출 가능
// error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
// queue.push('1');
queue.push(+'1'); // 실수를 사전 인지하고 수정할 수 있다
console.log(queue.pop().toFixed()); // 0
console.log(queue.pop().toFixed()); // 1
이렇게 하면 해당 위의 문제는 해결되지만, 새로운 기능의 클래스를 생성할때마다, 매번 타입 별로 클래스를 함께 생성해야한다는 단점이 존재합니다. 해당 Queue
클래스를 제네릭을 사용하여 바꿔봅시다.
class Queue<T> {
protected data: Array<T> = [];
push(item: T) {
this.data.push(item);
}
pop(): T | undefined {
return this.data.shift();
}
}
// number 전용 Queue
const numberQueue = new Queue<number>();
numberQueue.push(0);
// numberQueue.push('1'); // 의도하지 않은 실수를 사전 검출 가능
numberQueue.push(+'1'); // 실수를 사전 인지하고 수정할 수 있다
// ?. => optional chaining
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining
console.log(numberQueue.pop()?.toFixed()); // 0
console.log(numberQueue.pop()?.toFixed()); // 1
console.log(numberQueue.pop()?.toFixed()); // undefined
// string 전용 Queue
const stringQueue = new Queue<string>();
stringQueue.push('Hello');
stringQueue.push('World');
console.log(stringQueue.pop()?.toUpperCase()); // HELLO
console.log(stringQueue.pop()?.toUpperCase()); // WORLD
console.log(stringQueue.pop()?.toUpperCase()); // undefined
// 커스텀 객체 전용 Queue
const myQueue = new Queue<{name: string, age: number}>();
myQueue.push({name: 'Lee', age: 10});
myQueue.push({name: 'Kim', age: 20});
console.log(myQueue.pop()); // { name: 'Lee', age: 10 }
console.log(myQueue.pop()); // { name: 'Kim', age: 20 }
console.log(myQueue.pop()); // undefined
제네릭은 선언 시점이 아닌 생성 시점에서 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용 가능하도록 하는 기법 입니다. 한번의 선언으로 다양한 타입을 재사용이 가능하기 때문에, 해당 컴포넌트 혹은 클래스 생성 시, 타입 별로 반복하여 코드를 작성할 필요가 없어집니다.
참고
타입스크립트 제네릭 :
https://poiemaweb.com/typescript-generic
https://www.typescriptlang.org/ko/docs/handbook/2/generics.html