프론트엔드/TypeScript

[TypeScript] 제네릭 (Generic)

제네릭은 타입을 일반화하여 재사용성을 높이는 프로그래밍 기법입니다.

이를 통해 함수, 클래스 등을 다양한 타입에 대해 범용적으로 사용할 수 있게 됩니다.

 

 


제네릭 (Generics)

제네릭은 타입을 선언 시점이 아닌 생성 시점에 결정하는 프로그래밍 기법입니다.

이를 통해 함수, 클래스, 인터페이스 등을 다양한 타입에 대해 재사용할 수 있습니다.

 

제네릭의 기본 구조와 호출 방식은 다음과 같습니다.

T는 Type의 약자로써 주로 관용적으로 사용하는 타입 변수입니다.

function getValue<T>(value: T): T {
  return value;
}

getValue<string>('hello');
getValue<number>(10);
getValue<boolean>(true);

 

둘 이상의 타입 변수는 주로 T 다음의 알파벳 순서대로 표현합니다.

function getValues<T, U>(a: T, b: U): [T, U] {
  return [a, b];
}

 

 


등장 배경

단순히 매개변수를 그대로 반환하는 함수를 예로 들어보겠습니다.

이 함수는 string 타입만 인자로 받을 수 있기 때문에 범용성이 떨어집니다.

function getStringValue(value: string): string {
  return value;
}

 

그래서 다른 타입들을 받고 싶다면 함수를 추가로 선언해야 하는 불편함이 있습니다.

function getNumberValue(value: number): number {
  return value;
}

function getBooleanValue(value: boolean): boolean {
  return value;
}

 

그렇다면 any 타입을 쓰면 어떨까요?

function getAnyValue(value: any): any {
  return value;
}

any 타입은 어떤 타입이든 인자로 받을 수 있다는 점에서 제네릭과 비슷합니다.

그러나 함수의 매개변수와 리턴 값에 대한 타입 정보를 알 수 없습니다.

또한 any 타입은 타입 검사를 하지 않아 에러가 발생할 위험이 있습니다.

 

이러한 문제를 해결하기 위해 제네릭이 등장했습니다.

 

 


제네릭 인터페이스

제네릭을 인터페이스에 사용될 수 있습니다.

interface Foo<T, U> {
  x: T;
  y: U;
}

 

제네릭을 사용한 함수 인터페이스는 2가지 방식으로 표현 가능합니다. 

interface Bar<T> {
  (text: T): T;
}

interface Bar {
  <T>(text: T): T;
}

 

 

제네릭 클래스

제네릭을 클래스에 사용하여 클래스 내부가 일관적인 타입을 가지도록 할 수 있습니다.

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

  constructor() {}

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

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

 

 

제네릭 화살표 함수

리액트에서 제네릭을 화살표 함수로 구현하면 에러가 발생합니다.

tsx 파일에서는 <T>를 태그로 인식하기 때문입니다.

이를 해결하려면 extends를 사용하여 제네릭임을 알려야 합니다.

// Error
const func = <T>(arg: T): T => {
  return arg;
}

// Ok
const func = <T extends {}>(arg: T): T => {
  return arg;
};