generic 정의와 제한!

Posted by : on

Category : typeScript


한가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용됩니다.

타입의 변수화이다.

제너릭 타입은 타입에 유연성을 제공하여 컴포넌트 등에서 재사용을 가능하게 해주는 타입이다.
타입의 유연성이란 :string, :number 등과 같이 고정된 타입이 아닌 사용에 따라 여러 타입을 사용하게 해준다는 것이다.

이는 any 타입과 매우 흡사하지만 차이점이 있다.

제너릭은 타입 정보가 동적으로 결정되는 타입이다.

제너릭 타입은 다양한 타입을 받을 수 있다는 유연성이란 점에서 any 타입과 흡사하지만 타입의 정보가 동적으로 결정된다는 차이가 있다.

그리고 유니온 타입으로 여러개 지정하지 않는 이유는 타입이 정확하게 지정되야, 함수에서 에러가 안난다. string|number로 지정해 놓고 만약에 string을 던지고 그 안에서 spilit을 호출해도 string이 아닐수도 있기 때문에 에러가 나는 것이다.

제네릭의 한 줄 정의와 예시

제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미합니다. 아래 코드를 보겠습니다.

function getText(text) {
  return text;
}

위 함수는 text라는 파라미터에 값을 넘겨 받아 text를 반환해줍니다. hi, 10, true 등 어떤 값이 들어가더라도 그대로 반환합니다.

getText('hi'); // 'hi'
getText(10); // 10
getText(true); // true

이 관점에서 제네릭을 한번 살펴보겠습니다.

function getText<T>(text: T): T {
  return text;
}

위 함수는 제네릭 기본 문법이 적용된 형태입니다. 이제 함수를 호출할 때 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있습니다.

getText<string>('hi');
getText<number>(10);
getText<boolean>(true);

제네릭을 사용하는 이유

  • 타입이 고정되는 것을 방지하고 재사용 가능한 요소를 선언할 수 있다.
  • 타입 검사를 컴파일 시간에 진행함으로써 타입 안정성을 보장.
  • 캐스팅 관련 코드를 제거할 수 있다.
  • 제네릭 로직을 이용해 타입을 다르게 받을 수 있는 재사용 코드를 만들 수 있다.

또 다른 예제를 살펴보겠습니다.

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

위 코드는 인자를 하나 넘겨 받아 반환해주는 함수입니다. 마치 리눅스의 echo 명령어와 같은 역할을 하죠. 여기서 이 함수의 인자와 반환 값은 모두 string으로 지정되어 있지만 만약 여러 가지 타입을 허용하고 싶다면 아래와 같이 any를 사용할 수 있습니다.

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

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

이러한 문제점을 해결할 수 있는 것이 제네릭입니다. 아래 코드를 보겠습니다.

function logText<T>(text: T): T {
  return text;
}

먼저 함수의 이름 바로 뒤에 <T> 라는 코드를 추가했습니다. 그리고 함수의 인자와 반환 값에 모두 T 라는 타입을 추가합니다. 이렇게 되면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 됩니다. 따라서, 함수의 입력 값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있게 됩니다.

그리고 이렇게 선언한 함수는 아래와 같이 2가지 방법으로 호출할 수 있습니다.

// #1
const text = logText<string>("Hello Generic");
// #2
const text2 = logText("Hello Generic");

보통 두 번째 방법이 코드도 더 짧고 가독성이 좋기 때문에 흔하게 사용됩니다. 그렇지만 만약 복잡한 코드에서 두 번째 코드로 타입 추정이 되지 않는다면 첫 번째 방법을 사용하면 됩니다.

함수에서 제너릭 정의

제너릭은 <> 기호를 이용해서 정의하며, 이름은 자유롭게 지정할 수 있다.
function 함수이름 <제너릭이름>(인수: 제너릭 이름) : 제너릭 이름 {}


타입을 제한하자 extends

제네릭은 사용하는 시점에 타입을 결정해줌으로써 사실상 아무 타입이나 집어넣어도 상관 없다.

function identity<T>(p1: T): T {
   return p1;
}

identity(1);
identity('a');
identity(true);
identity([]);
identity({});

이렇게 입력값에 대한 유연성을 확보했지만 각 함수에 대해 사용처에 따라서 입력값을 제한 할 필요가 생긴다.

가장 대표적인 예로 forEach() 라는 메소드제네릭을 이용하여 만든다고 쳤을때, 이 forEach() 는 배열을 순회하는 고차 함수니 반드시 원본값을 배열로 받을 필요가 있다.

또한 리액트와 같은 라이브러리의 메소드를 구현할때에도 입력 가능한 값을 범위를 제한하여 만든다. 예를 들어 리액트의 속성값 전체는 객체 타입만 허용된다.

이를 위해 타입스크립트의 제네릭은 적용되는 타입의 종류를 제한할 수 있는 기능을 제공한다.

다음과 같이 제네릭에 extends 키워드를 이용하면 제네릭 타입으로 입력할 수 있는 타입의 종류를 제한할 수 있다.

type numOrStr = number | string;
// 제네릭에 적용될 타입에 number | string 만 허용
function identity<T extends numOrStr>(p1: T): T {
   return p1;
}
identity(1);
identity('a');
identity(true); //! ERROR
identity([]); //! ERROR
identity({}); //! ERROR

속성을 제한하자 제약조건

단순히 사용성을 위해 제네릭 타입을 제한 하는 것 뿐만 아니라 로직에 의해서 어쩔수 없이 제한해야 하는 경우도 있다.

예를 들어 다음 코드를 보면, T에는 .length 프로퍼티가 없다고 오류가 뜨는데, 왜냐하면 우리 입장에선 제네릭 타입이니 그럴려니 하겠지만 컴파일러 입장에선 T 타입이 대체 무엇인지 모르기 때문에 그런 것이다.

function loggingIdentity<T>(arg: T): T {
   console.log(arg.length);
   return arg;
}

매개변수 제약조건

하나의 함수에서 제네릭은 여러개 지정해서 사용할 수 있다.

이를 이용해 각 매개변수마다 다른 제네릭 타입 조건 제한을 걸수 있다

function myfunc<T extends string, K extends number>(arg1: T, arg2: K): void {
   console.log(typeof arg1); // string
   console.log(typeof arg2); // number
}
myfunc('1', 2);

이를 응용하면 다음과 같이 로직을 짤 수 있다. (이부분은 약간 어려우니 집중해서 보길 바란다)

getProperty 라는 메소드가 있고, 이 함수는 _객체_와 _key이름_을 아규먼트로 받는데, 만일 객체에 존재하지 않는 key명을 입력받을 경우 오류를 내뿜는다.

조건 분기로 해결할수도 있겠지만 제네릭 자체에서 타입을 제한하면 된다.

여기서 핵심은 K extends keyof T 제네릭 타입인데, 제네릭 T에는 x변수(객체) 가 오게되는데 이 객체의 key값만 뽑아 keyof를 통해 유니온 타입으로 ~~’a’ ‘b’ ‘c’ ‘d’~~ 만들어주고 K 제네릭에 제한을 건다.
그러면 K 제네릭은 반드시 ~~’a’ ‘b’ ‘c’ ‘d’~~ 상수 타입만 올수있다. 이런식으로 타입 가드 장치를 거는것도 타입스크립트의 로직의 한 방법이다.

함수 제약조건

만일 일반 타입이나 인터페이스가 아닌 함수 자체를 제네릭 인자에서 받을수 있도록 제한하는 것이면 어떻게 선언할까?

매개변수에 콜백 함수를 받아들일때는 다음과 같이 제네릭 제약을 할 수 있다. (T 에는 해당 함수 자테 타입 형태로만 들어올 수 있다)


function translate<T extends (a: string) => number, K extends string>(x: T, y: K): number {
   return x(y);
}

// 문자숫자를 넣으면 정수로 변환해주는 함수
const num = translate((a) => { return +a; }, '10');
console.log('num: ', num); // num : 10

About 유재석
유재석

개발자 유재석 입니다. Web Developer.

Email : jaeseok9405@gmail.com

Website : https://github.com/yoo94