제네릭
- 정적 타입언어들의 경우 함수 또는 클래스 정의 시점에 매개변수나 반환값의 타입을 선언해야합니다.
- Typescript 또한 정적 타입언어이기 때문에 함수 또는 클래스를 정의하는 시점에 매개변수나 반환값의 타입을 선언해야합니다.
- 그러나 이를 정의하는 시점에 타입선언이 어려운 경우가 있습니다.
- 단일 타입이 아닌 다양한 타입에서 작동하는 컴포넌트를 작성할 때 사용자는 제네릭을 통해 여러 타입의 컴포넌트나 자신만의 타입을 사용할 수 있습니다.
제네릭 사용
identity 함수를 구현해서 제네릭을 사용해 봤습니다. identity함수는 인수로 뭐가 오든 그대로 반환하는 echo 명령 같은 함수입니다.
function identity(arg: any): any{
return arg;
}
any 타입을 쓰면 arg
가 어떤 타입이든 받을 수 있을 것입니다. 하지만 실제로 함수가 반환할 때 어떤 타입인지에 대한 정보를 잃게됩니다.
function identity<T>(arg: T): T {
return arg;
}
이 함수에 T라는 타입변수를 추가하면 T는 유저가 넘긴 인수의 타입을 캡처하고 이 정보를 나중에 사용할 수 있게 합니다.
이렇게 제네릭 identity 함수를 작성하고 나면, 두 가지 방법중 하나로 호출할 수 있습니다.
- 함수에 타입 인수를 포함한 모든 인수를 전달하는 방법
const output = identity<string>("myString");
- 가장 일반적인 방법으로 타입 인수 추론 사용.
컴파일러는 값을 보고 그 타입으로 T를 정합니다.const output = identity("myString");
제네릭 타입 변수 작업
제네릭을 사용하기 시작하면 identity와 같은 제네릭함수를 만들 때 컴파일러가 함수 본문에 제니릭 타입화된 매개변수를 쓰도록 강요합니다.
함수 호출 시마다 arg의 길이를 출력하려면
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}
처럼 쓰고 싶지만
이렇게 하면, 컴파일러는 오류를 발생시킵니다. arg에 는 length 멤버가 없는 다른 타입을 전달 할 수도 있기 때문입니다.
하지만 이 함수가 T가아닌 T의 배열에서 동작하도록 의도했다면 length 멤버는 사용할 수 있습니다. 즉 다른 타입들의 배열을 만드는 것처럼 표현할 수 있습니다.
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
나
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
return arg;
}
의 형태로 사용할 수 있습니다.
이렇게하면 logginIdentity는 타입 매개변수 T와 T배열의 인수 arg를 받고 T배열을 반환한다고 읽을 수 있습니다.
또 다른 예제
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());
console.log(queue.pop().toFixed());
Queue 클래스의 data 프로퍼티는 어떤 타입의 요소도 가질 수 있는 any타입의 배열입니다.
any[] 타입은 배열 요소의 타입이 모두 같지 않습니다.위의 예제에서는 data 프로퍼티가 number 타입만 포함하는 배열이라는 기대하에 toFixed() 라는 메서드를 사용했습니다. 따라서 string타입이 들어왔을 때 에러가 발생했습니다.
이런 문제 해결을 위해서 Queue 클래스를 상속해서 number 타입 전용 클래스를 정의해서 사용할 수 있습니다.
class Queue {
protected data: any[] = [];
push(item: any) {
this.data.push(item);
}
pop() {
return this.data.shift();
}
}
class NumberQueue extends Queue{
push(item: number) {
super.push(item);
}
pop(){
return super.pop();
}
}
const queue = new NumberQueue();
queue.push(0);
queue.push('1');
console.log(queue.pop().toFixed());
console.log(queue.pop().toFixed());
이렇게 하면 의도치 않은 실수를 사전에 검출할 수 있습니다.
하지만 Number 뿐만 아니라 다양한 타입을 지원해야한다면??
타입 별로 클래스를 상속받아서 추가해야하므로 좋은 방법은 아닙니다.
제네릭을 사용해 봅니다.
class Queue<T> {
protected data: Array<T> = [];
push(item: T) {
this.data.push(item);
}
pop(): T | undefined {
return this.data.shift();
}
}
const numberQueue = new Queue<number>();
numberQueue.push(0);
numberQueue.push('1');
console.log(numberQueue.pop().toFixed());
console.log(numberQueue.pop().toFixed());
마찬가지로 의도치 않은 실수를 사전에 검출이 가능합니다. 이번에는 같은 클래스를 상속받아서 다른 타입의 큐를 만들어 봤습니다.
const stringQueue = new Queue<string>();
stringQueue.push('Hello');
stringQueue.push('World');
console.log(stringQueue.pop()?.toUpperCase());
console.log(stringQueue.pop()?.toUpperCase());
console.log(stringQueue.pop()?.toUpperCase());
제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한타입을 사용할 수 있도록 하는 기법입니다.
한 번의 선언으로 다양한 타입에 재사용이 가능합니다.
제약조건
제약 조건을 걸어 .length 프로퍼티가 있는 타입에서 동작하는 것을 제한할 수 있습니다.
interface Lengthwise{
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
제약 조건을 명시하는 인터페이스를 만들고 제약사항을 extends 키워드로 표현한 인터페이스를 이용해 명시해줍니다.
'TypeScript' 카테고리의 다른 글
[Typescript] 클래스, 추상클래스, 인터페이스, 그리고 차이 (0) | 2021.12.09 |
---|---|
TypeScript union타입, any타입, unknown 타입 (0) | 2021.10.25 |
TypeScript 설치와 타입지정 (0) | 2021.10.24 |
댓글