chap.6 제네릭

유원근·2020년 12월 23일
0
post-custom-banner


제네릭을 사용하면 함수가 파라미터를 받듯이 타입을 파라미터화 해서 여러가지 타입을 받아줄 수 있습니다.

즉 여러번 재사용이 가능한걸 만들때 사용할 수 있겠죠?

사용

왜 제네릭을 써야하지?

아래 코드는 인자를 하나 넘겨 받아 반환해주는 함수입니다.

function logText(text: string): string {
  return text;
}

tring으로 지정되어 있지만 만약 여러 가지 타입을 허용하고 싶다면 아래와 같이 any를 사용할 수 있습니다.

function logText(text: any): any {
  return text;
}

이렇게 타입을 바꾼다고 해서 함수의 동작에 문제가 생기진 않습니다.
하지만, 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지는 알 수가 없습니다.

왜냐하면 any라는 타입은 타입 검사를 하지 않기 때문입니다.

제네릭은 위와 같은 상황에서의 문제점을 근본적으로 해결할 수 있습니다.


제네릭 적용

먼저 Promise를 만들어 2번째 인자로 넘겨준 시간만큼 뒤에 첫번째로 넘겨준 X를 resolve하는 함수를 만들어줍니다.

function createPromise(x, timeoute: number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, timeoute);
  });
}

이렇게 하면 다음과 같이 1초뒤에 1을 콘솔창에 찍어줄 수 있겠죠?
하지만 지금 시점에서 x의 인자로 무엇이 들어올지는 알수가 없습니다.

createPromise(1, 1000).then((v) => console.log(v));

그런데!! 만약 위 createPromise함수에서 매개변수 x의 타입을 number로 지정해버리면 인자로 숫자밖에 넣을 수 없습니다.

이런 경우에는 제네릭을 이용해 타입변수를 사용해 줄 수 있습니다.

function createPromise<T>(x: T, timeoute: number) {
  return new Promise((resolve: (v: T) => void, reject) => {
    setTimeout(() => {
      resolve(x);
    }, timeoute);
  });
}

<T> 는 타입의 파라미터이기 때문에 해당 함수 어디서든 사용해 줄 수 있습니다.

그렇다면 <T>를 어떻게 어디에서 정의해 주어야 할까요?

createPromise<string>("string", 1000)
    .then((v) => console.log());

우선 위와 같은 방법으로 직접 타입을 지정해 줄 수 있습니다.

createPromise("string", 1000)
    .then((v) => console.log());

이렇게만 사용해도 첫번째 인자의 타입이 T이기 때문에 T는 문자타입이 지정되겠죠?
또한 Promise함수를 생성할때도 다음과 같이 그냥 명시해 줄 수 있습니다 .

function createPromise<T>(x: T, timeoute: number) {
  return new Promise<T>((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, timeoute);
  });
}

여러개의 제네릭 동시사용

함수의 파라미터를 정의할때 여러개를 정의할 수 있듯이, 제네릭에서도 여러개의 타입을 넣어 줄 수 있습니다.
간단하게 튜플을 만들어주는 함수를 만들어 볼까요?

function createTuple<T, U>(v: T, v2: U): [T, U] {
  return [v, v2];
}

const t1 = createTuple(1, 2);

이렇게 된다면 [숫자,숫자] 형의 tuple이 생성이 될 것입니다.
물론 아래와 같이 3개짜리 튜플도 생성이 가능합니다!

function createTuple<T, U, D>(v: T, v2: U, v3: D): [T, U, D] {
  return [v, v2, v3];
}

const t1 = createTuple(1, 2, 3);

클래스와 인터페이스에서의 제네릭

제네릭은 클래스와 인터페이스에서도 사용할수 있습니다!

먼저 interface를 하나 만들어주세요.

interface DB<T> {
  add(v: T): void;
  get(): T;
}

그리고 interface를 참조하는 class D를 만들어줍니다.

class D<T> implements DB<T> {
  add(v: T): void {
    throw new Error("Method not implemented");
  }
  get(): T {
    throw new Error("Method not implemented");
  }
}

class에서 interfaceimplements할때는 꼭 로 타입을 지정한 것처럼 둘다 지정 해 주어야만 합니다.

즉 인터페이스에서도 아직 정의되지는 않았지만 타입을 가지고 있는것으로 정의할 수 있고 그걸 class에서 implements를 할때 실질적으로 구현할때도 그 제네릭타입을 이용해 코딩할수 있습니다.

조건부 타입

흔히 사용하는 삼항연산자를 통해 조건부로 타입을 지정해 줄 수도 있습니다.

예를들어 야채와 고기, 그리고 카트라는 인터페이스를 하나 만들어주세요.

interface Vegitable {
v: string;
}
interface Meat {
m: string;
}
interface Cart<T> {
getItem(): T extends Vegitable ? Vegitable : Meat;
}

getItem의 반환타입 T는 반약 타입이 야채가 들어왔을 경우에는 야채이지만, 나머지 모든 경우에는 고기로 보겠다 라는 의미를 가지게 됩니다.

그럼 제네릭으로 야채가 아닌 문자열을 지정하면 어떻게 될까요?

const cart1: Cart<string> = {
getItem() {
  return {m: "고기"};
},
};

위와같이 return값 조건부 타입에 의해 고기가 되어서 m이 있는 객체 즉 Meat인터페이스 타입이 반환되어야 합니다.
반대로 야채를 넣어주아래와 같은 에러가 출력됩니다.

조건부 타입에 의해 반환타입은 야채이니 {v:’~~’} 를 반환해라 라는 의미겠죠?

조건부 타입을 이용해 특정한 메소드에서 반환되는 타입을 제네릭에 의해 다르게 결정되도록 만들 수 있는 것입니다.

제네릭을 사용하면 하나의 타입이 아닌 여러타입을 사용할 수 있게, 즉, 고정된 타입이 아니라 유동적으로 타입을 변경해줄수 있으며, 동시에 타입세이프한 코드를 작성할 수 있도록 도와주게 됩니다.

post-custom-banner

0개의 댓글