Erro & TIL. [TS] 함수 리턴 값이 2가지 이상 일때, Generics 으로 타입 지정하기

배상건·2022년 9월 22일
0

에러 관리

목록 보기
3/4

유니온 타입(Union Type)이란 자바스크립트의 OR 연산자(||)와 같이 A이거나 B이다 라는 의미의 타입입니다.

상황

type News = {
  id: number;
  time_ago: string;
  title: string;
  url: string;
  user: string;
  content: string;
};

// Intersection Type

type NewsFeed = News & {
  comments_count: number;
  points: number;
  // optional properties
  read?: boolean;
};

type NewsDetail = News & {
  comments: NewsComment[];
};

// 함수 getData의 반환값을 Union Type으로 지정
function getData(url: string): NewsFeed[] | NeswDetail {
  ajax.open("GET", url, false);
  ajax.send();

  return JSON.parse(ajax.response);
}

// 함수 getData가 사용되는 두가지 상황 발생
// 1. NewsFeed[]를 반환하는 함수 newsFeed
// 2. NeswDetail을 반환하는 함수 newsDetail
function newsFeed(): void {
  ...
  if (newsFeed.length === 0) {
    // 불러온 뉴스 데이터에 read 속성을 추가함
    newsFeed = store.feeds = makeFeeds(getData(NEWS_URL));
    console.log("makeFeeds read =>", newsFeed);
  }
}

function newsDetail(): void {
  const id = location.hash.substring(7);
  const newsContent = getData<NewsDetail>(CONTENT_URL.replace("@id", id));
  ...
}
  • 함수 getData는 상황에 따라, 타입 앨리어스로 지정한 NewsFeed[] 타입 혹은 NeswDetail 타입을 반환값으로 갖습니다.
  • 이를 표현하기 위해 함수 getData의 반환값으로 유니온 타입을 선언했습니다.
function getData(url: string): NewsFeed[] | NeswDetail {}

의도

  • NewsFeed[] 타입을 반환값으로 갖는 함수 newsFeed와 NeswDetail 타입을 반환값으로 갖는 함수 newsDetail 등 두가지 상황에서 각각 다른 함수 getData의 반환값이 적용될 것을 예상했습니다.
  • 이유는 유니온 타입(A | B)이 A도 될 수 있고 B도 될수 있는 타입이므로 함수 getData의 반환값 타압이 NewsFeed[]도 되고 NeswDetail도 될 것으로 예상했습니다.
  • 의도되로 된다면, 함수 getData의 반환값을 갖는 변수 newsContent는 title 속성이 존재해야할 것이며, 함수 newsFeed와 함수 newsDetail 모두 상황에 따라, 의도된 값을 가져야 합니다.

오류 발생

function getData(url: string): NewsFeed[] | NewsDetail
'NewsFeed[] | NewsDetail' 형식의 인수는 
'NewsFeed[]' 형식의 매개 변수에 할당될 수 없습니다.
  'NewsDetail' 형식에 'NewsFeed[]' 형식의 
  length, pop, push, concat 외 29개 속성이 없습니다.ts(2345)

원인

  • 타입스크립트 관점에서는 함수 getData를 호출하는 시점에 NewsFeed[] 타입이 올지 NewsDetail 타입이 올지 알수 없다고 합니다.

    이에, 어느 타입이 들어오든 우류가 나지 않는 방향으로 타입을 추론하게 되기 때문에, NewsFeed[] 과 NewsDetail 두 타입에 공통으로 들어있는 속성인 News 타입 속성만 접근할 수 있습니다.
    참고 : 타입스크립트 핸드북

해결 방법

함수 getData는 리턴 값을 2가지 종류를 갖고 있습니다.
그러나, getData를 사용하는 입장에서는, 어떤 값이 올지 예상할 수 없는 상황입니다.

  1. 타입 가드(Type Guard)를 이용하여 타입의 범위를 좁혀줍니다.
    그러나, 이는 좋은 방법이 아닙니다.
    왜냐하면, 함수 getData가 지금은 API 2개만 처리하지만, 추후, API 30개를 처리한다면, 함수 getData를 호출할 때마다, 조건문으로 끝 없이 타입 가드 코드를 작성해야 하기 때문입니다.
    즉, "리턴 값으로 유니온 타입으로 기술하는 것만으로는 쉽지 않다."라는 것을 알 수 있습니다.
  2. 제네릭을 적용합니다.

제네릭 사전적 의미

제네릭은 C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징입니다. 특히, 한가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용됩니다.
출처 : 타입스크립트 핸드북

제네릭은 입력이 n개의 유형일 때, 출력도 n개의 유형인 것을 정의하는 것입니다.
여기서 핵심은 입력이 ABCD 유형 중, A가 입력되면 출력도 A로 나가야합니다.

즉, 입출력이 동일한 유형이어야 합니다.

이러한 상황을 제네릭이라는 문법으로 표현할 수 있습니다.

제네릭 적용하기

function getData<AjaxRespons>(url: string): AjaxRespons {
  ajax.open("GET", url, false);
  ajax.send();

  return JSON.parse(ajax.response);
}
  1. 함수의 이름 바로 뒤에 "T" 라는 코드를 추가하고, 함수의 인자와 반환 값 모두에 "T" 라는 타입을 추가합니다.

  2. 이때, "T" 는 통상적인 이름이며, 명시적으로 지정하는 것이 가독성을 높혀 줄 수 있습니다. 여기서는 "T" 대신 "AjaxRespons" 로 작명 했습니다.

  3. 또한, 위 코드에서는 반환 값 타입만 지정해줬습니다. 이유는 제네릭의 특성 상, 입출력 타입이 모두 동일하기 때문에, 인자 타입을 생략했습니다.

    if (newsFeed.length === 0) {
      // 불러온 뉴스 데이터에 read 속성을 추가함
      newsFeed = store.feeds = makeFeeds(getData<NewsFeed[]>(NEWS_URL));
      console.log("makeFeeds read =>", newsFeed);
    }
    
    function newsDetail(): void {
    	 const id = location.hash.substring(7);
    	 const newsContent = getData<NewsDetail>(CONTENT_URL.replace("@id", id));
    ...
    }
    
  4. 제네릭이 적용된 함수 getData를 호출 할 때, 기술된 타입이 그대로 "AjaxRespons 타입" 에 적용되고, 리턴 값을 "AjaxRespons 타입"으로 출력합니다.

  5. 즉, 호출하는 쪽에서 타입을 명시해 주면, 그 타입을 적용해서 그대로 getData의 반환 타입으로 사용하겠다는 것이 제네릭입니다.

profile
목표 지향을 위해 협업하는 개발자

0개의 댓글