Generics

이전에 C++ 를 이용해서 Stack, List, Map 등의 컨테이너를 구현했던 경험이 있다. 이런 컨테이너를 구현할 때 가장 중요했던 두가지가 있었는데 첫 번째는 자료구조와 알고리즘, 두번째가 바로 제네릭 이었다.

제네릭을 이용하면 하나의 함수 또는 클래스로 여러 타입의 인자를 다룰 수 있게 된다. 따라서 재사용이 가능하게 되는 엄청난 장점이 있었다. 타입스크립트에도 이런 제네릭이 있다. 당장 학습하지 않을 이유가 없다.

제네릭을 왜써? 우리는 any가 있잖아!!

부끄럽지만 제네릭이라는 키워드를 처음봤을 때 이런 생각을 했었다. 제네릭을 왜 쓰지? 그냥 any 를 사용하면 되는거 아니야? 내 이런 마음을 간파라도 했듯이 공식문서의 Generic 문서 가장 첫 부분에서 이 문제를 짚어준다.

While using any is certainly generic in that it will cause the function to accept any and all types for the type of arg, we actually are losing the information about what that type was when the function returns. If we passed in a number, the only information we have is that any type could be returned.

공식문서의 내용을 가져와봤다. 요약하자면 이렇다. any 도 확실히 제네릭이다. 하지만 any 는 타입에 대한 정보를 알려주지 못한다! 예를 들어보자.

function identity(arg: any): any {
  return arg;
}
const num = 10;
const ret = identity(num);

ret 은 어떻게 타입추론이 되었을까? 당연히 number 로 되지 않았을까? 당연히 아니다! ret 의 타입은 any다.any 를 사용하게 되면 타입에 대한 정보를 잃어버리게 되고 따라서 우리가 원하는 바를 이루지 못한다. 즉 any 는 generic 를 대체할 수 없다!

자 그럼 맛보기로 제네릭을 사용해서 위의 문제를 해결하자.

function identity<T>(arg: T): T {
  return arg
}

제네릭의 핵심은 타입에 대한 정보를 기억한다는거다.

Generic Types 적용

interface

자 이제 그러면 interface 에 제네릭을 적용해보자. 위의 identity 라는 함수를 interface 로 나타내보자.

interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg
}

핵심은 심플하다. interface 도 제네릭을 받을 수 있다는 것이다. 따라서 일반함수처럼 <Generic Parameter> 를 넣어주면 된다!

Classes

클래스에 적용하는 방법도 매우 간단하다! 그냥 하던것처럼 하면됨...

class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

Generic Constraints

Constraints 에 대해 알아보기 전에 우선 아래의 경우를 보자.

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

위의 코드는 에러가 날까? 나지 않을까? 정답은 에러가 난다. 그러면 무슨 에러가 날까? 에러 메시지를 보면 Property 'length' does not exist on type 'Type'. 이렇다.

즉, arg 의 인자가 length 를 가지는지를 현재로써는 알 수 없으니깐, 그리고 length 프로퍼티가 없는 인자가 들어올 수도 있으니 에러가 난다!! 그러면 아래처럼 하면 안될까?

function loggingIdentity<Type>(arg: Type): Type {
  if (!arg.length) {
    console.log("I don't have length!");
    return;
  }
  console.log(arg.length);
  return arg;
}

물론 이렇게 해도 괜찮다. 하지만 이건 좋지 않아? Generic 을 이용해서 멋지게 만들수있다! 아래를 보자.

interface LengthWise {
  length: number;
}

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

짜잔~ 이제 우리는 인자로 들어오는 T 라는 타입에 length 라는 프로퍼티가 항상 있다고 가정한 상태에서 코딩을 할수있게 된다! 그대신 이전처럼 모든 타입의 인자가 들어올 수는 없다. 왜? 제한을 걸었으니깐!

조금 더 업그레이드해서 이번에는 키 - 밸류 를 갖는 객체에서 키값을 이용해 밸류값을 가져오는 함수를 만들고, 해당 함수에 타입을 넣어보자.

// 일반 자바스크립트
function getProperty(obj, key) {
  return obj[key]
}

우선 일반 자바스크립트로 구현했다. 이제 타입을 생각해보자. 여기서 확신할 수 있는 한가지는 key의 value 는 값의 타입은 사용하기 전까지는 알 수 없다는 사실이다. 즉 우리는 제네릭을 사용해서 일반화해야한다. 자 그럼 이제 일반화해보자.

일반화하려면 무엇을 알아야할까? 간단하다. keyof 라는 키워드만 알면된다. keyof 는 간단히 객체의 key 타입을 가져와주는 키워드다. 첫번째 인자의 타입이 T 라면 두번째 인자인 key 의 타입은 key of T 가 된다!!

// keyof 라는 타입을 알았으니...
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");
getProperty(x, "m"); // 에러발생

Last updated