제네릭은 Java 등의 정적 타입 언어를 사용하던 사람에게는 익숙한 단어 일지 모른다.

제네릭은 어떠한 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 기법이다. 선언 시점에서 타입을 정의하는 것이 아니라 사용 시점에 정하기 때문에 함수나 클래스를 범용적으로 사용할 수 있다.

제네릭을 사용하는 이유

class Stack {
  private data: any[] = [];

  constructor() {}

  push(item: any): void {
    this.data.push(item);
  }

  pop(): any {
    return this.data.pop();
  }
}

이러한 스택 구조가 있을 때 어떠한 값이 들어 올지 모르기 때문에 any 타입으로 지정했다. 그러나 타입의 추론이 불가하고 데이터의 타입이 제각각이 될 수 있다. 그래서 런타임에 타입을 검사 하는 코드가 필요해 진다.

그렇다고 자료형을 보장하기 위해 number만 받는다면? 범용성이 떨어 진다. 이런 경우 제네릭을 사용하여 해결 할 수 있다.

class Stack<T> {
  private data: T[] = [];

  constructor() {}

  push(item: T): void {
    this.data.push(item);
  }

  pop(): T | undefined {
    return this.data.pop();
  }
}

T는 Type의 약자로 다른 언어에서도 제네릭을 선언할 때 관용적으로 많이 사용된다. 여기에서 T를 타입 변수(Type variable) 라고 한다.

사용방법은 아래와 같이 생성자를 호출하여 인스턴스를 만들 때 T로 사용될 타입을 꺽쇠에 지정해 주면 된다.

const s = new Stack<number>();
s.push(1);

함수

배열을 입력으로 받아 그 배열의 첫번째 요소를 출력하는 lodash.head() 같은 함수를 구현해 보자. 제네릭을 사용하지 않는 경우 이렇게 할 것이다.

function first(arr: any[]): any {
  return arr[0];
}

리턴 타입을 알 수가 없다. 바꿔 보자.