logo
June Kim

TypeScript의 지연평가 - Generic

이전에 다른 개발자분들과 함께 Effective typescript 스터디를 진행했었던 적이 있습니다. 최근 이직을 하며 새로운 회사에 안착하게 되었는데 입사한 회사에서도 사내 Front 개발자분들과 함께 Effective typescript, 우아한 타입스크립트 책을 가지고 TS 스터디를 진행하게 되었습니다.

다시 봐도 Effective typescript는 인사이트가 되는 내용이 되게 많았습니다. 그중에서도 Generic에 대해서 한번 더 학습하다가 얻게된 인사이트가 있어서 글을 적게 되었습니다.

Generic 이란?

제네릭은 어떠한 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 프로그래밍 기법을 말합니다. 아마도 타입스크립트를 꽤나 다루어 보신분이라면 제네릭에 대한 개념은 어느정도 있으실텐데요. 저 또한 제네릭을 꽤나 사용했던 경험이 있었습니다.

그런데 Effective typescript 에서 [아이템 33 string 타입보다 더 구체적인 타입 사용하기] 에서 나온 제네릭 예시 코드를 살펴보다가 조금 헷갈리는 문제가 나와서 들여다 보았는데요. 아래와 같습니다.

function pluck<T>(records: T[], key: keyof T) {
  return records.map(r => r[key]);
}

function pluckNarrowed<T, K extends keyof T>(records: T[], key: K) {
  return records.map(r => r[key]);
}

interface Album {
  artist: string;
  title: string;
  releaseDate: Date;
  recordingType: 'studio' | 'live';
}
const albums: Album[] = [
  {
    artist: 'A',
    title: 'A album',
    releaseDate: new Date(),
    recordingType: 'live',
  },
  {
    artist: 'B',
    title: 'B album',
    releaseDate: new Date(),
    recordingType: 'studio',
  },
];

const types = pluck(albums, 'recordingType');
const types2 = pluckNarrowed(albums, 'recordingType');

해당 코드에서 types 와 types2 의 타입 추론이 어떻게 될까요? 한번 잠깐 고민해보시면 좋을것 같습니다.


























정답은

const types: (string | Date)[];
const types2: ('studio' | 'live')[];

로 추론이 됩니다.

왜 이런 차이가 있는지 하나씩 살펴보겠습니다.

pluck

pluck의 경우

function pluck<T>(records: T[], key: keyof T) {
  return records.map(r => r[key]);
}

pluck(albums);

records에 albums를 넣는 순간 T 라는 타입이 Album 이라는 것을 알 수 있고, key는 keyof Album 으로 TS에서 평가가 완료됩니다. 따라서 key에

‘artist’ | ‘title’ | ‘releaseDate’ | ‘recordingType’

4가지의 리터럴 타입이 들어올 수 있지만 무엇이 들어오든 간에 pluck 함수의 리턴 타입은 pluck(albums)에서 평가가 완료되었습니다

const types = pluck(albums, 'artist');
const types = pluck(albums, 'title');
const types = pluck(albums, 'releaseDate');
const types = pluck(albums, 'recordingType');

위의 4가지 타입은 모두 (string | Date)[] 로 추론이 됩니다.

pluckNarrowed

pluckNarrowed의 경우

function pluckNarrowed<T, K extends keyof T>(records: T[], key: K) {
  return records.map(r => r[key]);
}

pluckNarrowed(albums);

records에 albums를 대입해도 key는 keyof Album을 extends 하는 K 타입이 오도록 타입이 좁혀질 뿐, 위의 4가지 리터럴 유니온 타입중에 하나가 올 수 있어서 아직 평가가 끝나지 않게 됩니다.

pluckNarrowed(albums, 'recordingType');

key에 'recordingType' 를 넣는 순간 K 타입에 대한 평가가 완료되어 key가 recordingType 라는 좁혀진 리터럴 타입으로 추론 됩니다. 그래서 pluckNarrowed(albums, 'recordingType') 의 타입은 T,K 에 대한 타입 평가가 모두 완료되어야 리턴타입이 ('studio' | 'live')[] 로 추론이 되는거죠.

제네릭은 type의 지연평가라고 할 수 있을것 같습니다.

정답을 바로 맞추신 분들도 계셨겠지만 저는 처음보고 이해하는데 조금 시간이 걸렸습니다. Generic을 잘 알고 있다고 생각했는데 조금 부족했던거죠. 다른 분들도 이 예시를 보고 제네릭을 좀더 이해하는데 도움이 되었으면 좋겠습니다.