Please enable JavaScript to view the comments powered by Disqus.

TypeScript에서 generic과 index type의 활용

함수 파라미터의 타입을 추론하기

타입스크립트를 사용하면 타입을 직접 지정하는 것도 가능하지만 컴파일러의 추론을 통해 타입을 유연하게 지정하는 것도 가능하다.

예를 들어 아래와 같은 함수가 있다고 하자.

function omitByKey(obj, ...rest) {
  const isKeyMatches = target => rest.includes(target)

  return Object.entries(obj).reduce((result, [name, value]) => {
    return isKeyMatches(name) ? result : { ...result, [name]: value }
  }, {})
}

expect(omitByKey({ a: 1, b: 2 }, 'a')).toEqual({ b: 2 });

omitByKey는 객체에서 지정된 키를 제거하는 함수다. 파라미터 전개 연산자를 사용해서 삭제할 키를 자유롭게 추가할 수 있도록 했다.

이 함수에 타입을 추가하려고 한다면 rest 배열에 들어갈 수 있는 문자열의 집합을 obj 파라미터로 전달된 객체의 키로 한정하고 싶을 것이다. 그런데, 함수 작성 시점에서는 obj 에 어떤 키가 들어있을 지 결코 알 수가 없다.

이런 케이스를 위해 타입스크립트는 타입 추론을 위한 문법을 제공한다. 여기서는 제네릭(generic)과 인덱스 타입(index type)을 사용해서 추론이 가능하다.

제네릭

제네릭 타입은 함수 선언 시점에는 타입이 고정되어 있지 않으며 사용할 때 타입이 지정되는 형태를 말한다.

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

identity 함수 이름 옆에 <T> 를 추가해서 이 함수에서 T라는 이름으로 제네릭을 사용하겠다고 선언했다. 그리고 파라미터가 T 타입을 가지며, 리턴 값에도 T 타입을 부여했다. 이 함수는 파라미터 arg가 어떤 타입을 가졌느냐에 따라 리턴 타입이 유연하게 달라지게 된다. 만약 제네릭이 없다면 number, string 등 다양한 타입에 대한 identity 함수를 별도로 선언해야 할 것이다.

function identityNum(arg: number): number {
  return arg;
}

function identityStr(arg: string): string {
  return arg;
}

// 그리고 다른 타입도 계속...😓

위의 omitByKey 함수도 타입이 유연하게 지정되어야 하므로 제네릭이 필요하다. 그리고 또 하나, 객체가 가지고 있는 키를 가져오기 위해 인덱스 타입이 필요하다.

인덱스 타입(index type)

객체의 키로 타입을 만들 때는 index type query 연산자 keyof를 사용한다. 아래의 코드에서 타입 K는 객체 numbers의 키로 구성된 union 타입이 된다.

const numbers = {
  one: 1,
  two: 2,
  three: 3,
}

type K = keyof typeof numbers;  // type K = 'one' | 'two' | 'three'

그럼 필요한 도구는 준비가 되었으니 omitByKey에 타입을 지정해 보자.

제네릭과 인덱스 타입을 함께 사용하기

export default function omitByKey<T, K extends keyof T, R extends Omit<T, K>>(
  obj: T,
  ...rest: K[]
): R {
  const isKeyMatches = (target: K) => rest.includes(target);

  return Object.entries(obj).reduce((result, [name, value]) => {
    return isKeyMatches(name as K) ? result : { ...result, [name]: value };
  }, {} as R);
}

이 함수에는 T, K, R이라는 3개의 제네릭 타입을 사용했다. T는 객체의 타입에 지정될 것이고 K는 위의 예제에서 본 대로 객체 T의 키로 구성된 union 타입을 상속했다. R은 함수의 리턴 타입으로 Omit 유틸리티 타입을 활용했다.

이렇게 작성하면 rest 배열에는 obj 객체의 키에 해당하는 문자열만 들어갈 수 있다. 만약 다른 키를 넣으려 한다면 아래와 같이 컴파일러가 즉시 오류를 확인시켜 줄 것이다.

const numbers = {
  one: 1,
  two: 2,
  three: 3,
}

omitByKey(numbers, 'another')
// Argument of type 'another' is not assignable to parameter of type 'one' | 'two' | 'three' 

참고 자료