본문 바로가기
TypeScript

typescript 제네릭

by 새우하이 2022. 1. 8.

제네릭

  • 정적 타입언어들의 경우 함수 또는 클래스 정의 시점에 매개변수나 반환값의 타입을 선언해야합니다.
  • Typescript 또한 정적 타입언어이기 때문에 함수 또는 클래스를 정의하는 시점에 매개변수나 반환값의 타입을 선언해야합니다.
  • 그러나 이를 정의하는 시점에 타입선언이 어려운 경우가 있습니다.
  • 단일 타입이 아닌 다양한 타입에서 작동하는 컴포넌트를 작성할 때 사용자는 제네릭을 통해 여러 타입의 컴포넌트나 자신만의 타입을 사용할 수 있습니다.

제네릭 사용

identity 함수를 구현해서 제네릭을 사용해 봤습니다. identity함수는 인수로 뭐가 오든 그대로 반환하는 echo 명령 같은 함수입니다.

function identity(arg: any): any{
    return arg;
}

any 타입을 쓰면 arg가 어떤 타입이든 받을 수 있을 것입니다. 하지만 실제로 함수가 반환할 때 어떤 타입인지에 대한 정보를 잃게됩니다.

function identity<T>(arg: T): T {
    return arg;
}

이 함수에 T라는 타입변수를 추가하면 T는 유저가 넘긴 인수의 타입을 캡처하고 이 정보를 나중에 사용할 수 있게 합니다.
이렇게 제네릭 identity 함수를 작성하고 나면, 두 가지 방법중 하나로 호출할 수 있습니다.

  1. 함수에 타입 인수를 포함한 모든 인수를 전달하는 방법
const output = identity<string>("myString");
  1. 가장 일반적인 방법으로 타입 인수 추론 사용.
    const output = identity("myString");
    컴파일러는 값을 보고 그 타입으로 T를 정합니다.

제네릭 타입 변수 작업

제네릭을 사용하기 시작하면 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 키워드로 표현한 인터페이스를 이용해 명시해줍니다.

댓글