반응형

니콜라스 선생님의 타입스크립트 강의를 듣다가 제네릭을 추가적으로 정리해봅니다.

 

Generic

먼저 제네릭이란 다양한 모습의 함수를 지원하는 다형성을 만족시키기 위해서 나온 개념이다.

다양한 파라미터 자료형(컨크리트 타입)와 다양한 리턴값을 해당 함수에서 제공해야한다면 어떻게 작성해야 할까?

(*concrete type : 컨크리트 타입은 기본적으로 제공되는 타입들로 number, string, boolean, void, unknown등이 있다.)

 

제네릭을 사용하지 않는다면 아래와 같은 모습이 될 것이다.

 

 

- 일반 타입스크립트로 다형성을 만족하는 함수 만들기

type SuperPrint = {
    (arr: number[]): void
    (arr: boolean[]): void
    (arr: string[]): void
    (arr: (number|boolean)[]): void
}

const superPrint: SuperPrint = (arr) => {
    arr.forEach(i => console.log(i))
}

superPrint([1,2,3,4,5]); // 1번째
superPrint([true, false, true]); // 2번째
superPrint(["a", "B", "c"]); // 3번째
superPrint([1, 2, true, false]); // 4번째

각각 다양한 타입의 파라미터를 제공하는 모습이 생길때마다 call signature 정의부가 늘어난다.

number배열과 그것을 리턴하는 자료형, string 배열과 자료형, number, boolean이 혼합된 자료형 등등...

종류가 많아지면 점점 별칭이 많아질텐데 이런 경우가 많지는 않겠지만, 제네릭을 사용함으로써 함축시킬 수 있다.

(* call signature: 함수의 파라미터 구성과 return자료형을 표현한 type을 말한다.)

 

- Generic 사용해보기

<T>, <V> 형태로 주로 사용된다.

type SuperPrint = {
  <TypePlaceholder>(arr: TypePlaceholder[]): TypePlaceholder;
};

const superPrint: SuperPrint = (arr) => arr[0];

superPrint([1, 2, 3, 4, 5]); // 1번째
superPrint([true, false, true]); // 2번째
superPrint(["a", "B", "c"]); // 3번째
superPrint([1, 2, true, false]); // 4번째

각 배열의 첫번째 값을 리턴하도록 간단하게 작성해보았다.

 

여기서 이상한점으로 그럼 any랑 뭐가 다른거지? 라는 생각이 들 수도 있다.

그래서 바로 any로 작성해보고 차이점을 살펴보았다.

 

 

any와 Generic의 차이점은 무엇인가?

type SuperPrint = {
  (arr: any[]): any;
};

const superPrint: SuperPrint = (arr) => arr[0];

superPrint([1, 2, 3, 4, 5]); // 1번째
superPrint([true, false, true]); // 2번째
superPrint(["a", "B", "c"]); // 3번째
superPrint([1, 2, true, false]); // 4번째

똑같이 동작은 되지만, vs code에서 마우스 오버를 통해 각 메소드의 정보를 보면 바로 차이점이 눈에 보인다.

 

 

any는 모든 타입의 정보를 잃는다.

모든 메소드들 위에 올려보면 타입에 대한 정보가 존재하지 않는다 모든것이 any로 표현된다.

 

 

하지만 제네릭의 경우는 어떻게 보일까?

Generic은 각각 파라미터와 return타입을 표현해준다.

입력한 배열 정보에 의해 파라미터값과 return값의 타입의 정보를 보여주는걸 볼 수 있다.

 

 

마지막의 복잡한 정보도 마찬가지이다.

Generic은 각각 파라미터와 return타입을 표현해준다.

파라미터는 number, boolean이며 return또한 number, boolean임을 명시해준다.

 

그럼에도 이상함을 못느낀다면 아래와같은 예시는 어떨까?

type SuperPrint = {
	(arr: any[]): any;
};

const superPrint: SuperPrint = (arr) => arr[0];
const test = superPrint([1, 2, true, false]);
test.toUpperCase(); // ???! 1이라는 number한테 대문자로 변경하라는건 말이 안된다...

any는 저런 말이 안되는걸 오류로 감지하지 않는다.

Typescript의 보호를 받으면서 다형성을 유지하려면 generic 형태로 꼭 함수를 작성해주자.

generic은 잘못된 코드를 보호해주게 된다.

 


함수의 파라미터가 늘어날때 제네릭 표현법

파라미터가 늘어난다면 아래처럼 제네릭 이름만 정해서 늘려주면 된다.

type SuperPrint = {
  <T, V>(arr: T[], param?: V): T|V;
};

const superPrint: SuperPrint = (arr, param) => {
  if (param) {
    return param;
  } else {
    return arr[0];
  }
};

const test = superPrint([1,2,3,4], "test");
const test2 = superPrint(["hi", "hello"]);
console.log(test);
console.log(test2);

결과값

 

 

제네릭의 형태는 앞으로 react등의 라이브러리를 사용하다보면 이런 형태의 다양한 자료형을 제공하는 함수를 사용하면 볼 수 있을 것이다.

반응형