제네릭 타입

나현·2023년 3월 27일
0
post-thumbnail

급변하는 기술 시장에서 살아남기 위해 Typescript 공부를 시작하였습니다...
과연 저는 살아남을 수 있을까요?!

👀 들어가기에 앞서...

타입스크립트에는 제네릭 타입(generic type)이라는 것이 있습니다.
이번에는 이것이 무엇인지 알아보도록 하겠습니다.

👀 제네릭 타입이 뭘까요??

이름에서 오는 낯선 향기...
쉽게 말해 제네릭은 타입에 변수를 제공하고,
배열 안의 요소까지 어떤 타입을 가졌는지 표기할 때 사용합니다.
다음과 같이 <>를 이용해 표기합니다.

const numbersArr: Array<number>=[]; //number[] 와 같습니다.

배열의 경우
<> 안에는 배열 안에 들어갈 데이터들의 타입을 입력합니다.
만약 두가지 타입이라면 유니온 타입을,
여러 가지가 섞여있다면 any를 입력하면 됩니다.

배열 외에도 함수, 클래스 등에도 활용이 가능합니다.

👀 왜 제네릭 타입을 쓸까요?

제네릭 타입은 단순히 배열을 나타내는 것이 아니라
배열 안의 요소들에 대한 타입을 알려줍니다.
때문에 배열 안의 요소를 미리 파악하여 그에 대한 연산을 안전하게 할 수 있다는 장점이 있습니다.
제네릭 타입을 사용하면 보다 나은 타입 안전성을 확보할 수 있습니다.

👀 제네릭 함수 만들기

이번에는 제네릭을 활용하여 함수를 만들어 볼까요?
만약 일반 객체 2개를 매개변수로 받아 하나의 객체로 리턴하는 함수가 있다고 가정해 봅시다!

function mergeObj(obj1: object, obj2: object) {
  return Object.assign(obj1, obj2);
}

const result1 = mergeObj({ id: 20230001, name: 'stella' }, { class: 'IT' });

여기서 만약 result1의 속성에 접근하려고 하면 에러가 납니다.
왜 일까요???
타입스크립트가 추론하기에 반환되는 object는 어떤 속성을 가지고 있고,
각 속성들의 타입이 어떤지 확실하지 않기 때문입니다.

이 때 제네릭을 사용하면 변수를 타입으로 전달하여
타입스크립트가 객체에 어떤 속성을 가지고 있는지 확실하게 알 수 있습니다.
마찬가지로 <>를 사용해 위 예제를 아래와 같이 표시할 수 있습니다.

function mergeObj<T,U>(obj1: T, obj2: U) {
  return Object.assign(obj1, obj2);
}

const result1 = mergeObj({ id: 20230001, name: 'stella' }, { class: 'IT' });

정해진 것은 아니지만 통상적으로 대문자 T,U를 사용하며,
타입스크립트는 반환되는 타입을 T와 U의 인터섹션한 타입으로 받아들입니다.
이렇게 하면 타입스크립트가 함수에 의해 반환되는 객체의 속성을 명확하고 알 수 있고
에러가 나지 않습니다.

그런데 만약!

제약 조건

위 예제에서 전달인자로 숫자나 문자열 같은 원시타입을 전달하면 어떻게 될까요?
당연히 제대로 하나의 객체가 되지 않습니다.
받아야할 전달인자를 객체로 제약을 걸 순 없을까요?
바로 가능합니다!

function mergeObj<T extends object,U extends object>(obj1: T, obj2: U) {
  return Object.assign(obj1, obj2);
}

const result1 = mergeObj({ id: 20230001, name: 'stella' }, { class: 'IT' });

위 예제처럼 제네릭 타입에 extends 를 사용하면 전달인자의 타입에 제약 조건을 걸 수 있습니다.
extends 뒤에는 유니언 타입 등 어떤 타입이든 가능합니다!

👀 제네릭 클래스 만들기

제네릭 타입을 이용해 클래스를 만들 수 있습니다.
만약 특정한 값을 받아 이를 토대로 배열을 만드는 클래스가 아래 예제와 같이 있다고 가정해봅시다!

이 클래스는 배열의 요소들을 삽입하고, 삭제하고, 읽어들일 수도 있습니다.

class MakeArray{
  private arr: any[] = [];

  addItem(item) {
    this.arr.push(item);
  }

  removeItem(item) {
    if (this.arr.indexOf(item) === -1) {
      return;
    }
    this.arr.splice(this.arr.indexOf(item), 1);
  }

  getItems() {
    return [...this.arr];
  }
}

이 예제를 그냥 쓰면 오류가 납니다.
그리고 타입스크립트 배열 안의 요소로 어떤 것이 들어올지 추론이 어렵고
배열 안의 요소로 객체가 들어오면 위 예제의 코드는 좀 더 손봐야해요.

이런 문제들은 제네릭 타입을 이용해서 해결할 수 있습니다!

class MakeArray<T extends string | number | boolean> {
  private arr: T[] = [];

  addItem(item: T) {
    this.arr.push(item);
  }

  removeItem(item: T) {
    if (this.arr.indexOf(item) === -1) {
      return;
    }
    this.arr.splice(this.arr.indexOf(item), 1); // -1
  }

  getItems() {
    return [...this.arr];
  }
}

이렇게 제네릭 타입을 이용하면 안전하게 원시값만 받아들일 수 있고,
지금있는 코드를 조금만 수정하여 활용이 가능합니다.
이렇게 배열을 만드는 클래스를 잘 만들어 볼 수 있습니다!


😝 마무리

이번에는 내용이 좀 길죠?
생소한 개념인 제네릭 타입에 대해 알아보았습니다.
좀 어렵더라도 예제를 살펴보면 이해하면 이해가 좀 쉽습니다...!

저는 다음 시간에도 살아남을 수 있을까요?!😂

profile
프론트엔드 개발자 NH입니다. 시리즈로 보시면 더 쉽게 여러 글들을 볼 수 있습니다!

0개의 댓글