starkoora / wanted-pre-onboarding-challenge-fe-1

64 stars 1 forks source link

타입스크립트 : response.json()은 왜 any일까요? #16

Closed movie42 closed 2 years ago

movie42 commented 2 years ago

안녕하세요. useFetch라는 커스텀 훅을 사용해서 비동기 코드를 분리시키는 과정에서 최종적으로 return 되는 json객체에 type이 지정되지 않고 any라고 되는데요. 그 이유가 궁금해서 질문 드립니다. 타입은 제네릭으로 TBody와 TData를 받아서 Promise를 return하는데요 getData 함수의 return에 마우스를 올리면 data는 any라고 표시됩니다. 그래서 타입 단언을 했더니 해결은 되지만 이번 수업 과제에서 타입 단언을 없애는 것이 목표중 하나였기 때문에 왠지 찝찝합니다. any를 없애기 위해서는 타입 단언말고 방법이 없을까요?

재현 코드

예시 코드

const useFetch = <TBody, TData extends unknown>(baseUrl: string) => {
  const { token } = useRecoilValue(userState);

   // 분명 Promise<TData>를 리턴하는데 리턴값은 any입니다.
  const getData = async (subUrl: string): Promise<TData> => {
    const url = `${baseUrl}${subUrl}`;

    const response = await fetch(url, {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
        Authorization: token
      }
    });
    //  마우스를 올리면 type은 any입니다.
    const { data } = await response.json();
    //  마우스를 올리면 type은 any입니다.
    return data;
  };

  return { getData };
};

export default useFetch;
starkoora commented 2 years ago

@movie42 결론 부터 말씀드리자면 response.json의 리턴타입이 Promise<any> 라서 그렇습니다

// lib.dom.d.ts
interface Body {
    readonly body: ReadableStream<Uint8Array> | null;
    readonly bodyUsed: boolean;
    arrayBuffer(): Promise<ArrayBuffer>;
    blob(): Promise<Blob>;
    formData(): Promise<FormData>;
    json(): Promise<any>;
    text(): Promise<string>;
}

대신 아래 링크처럼 리턴된 객체에 타입을 주실 수 있습니다 아니면 그냥 TData를 활용해서 타입 단언을 하셔도 괜찮을거 같고요

참고 링크 : https://kentcdodds.com/blog/using-fetch-with-type-script#removing-anythings

starkoora commented 2 years ago

@movie42 유사한 예시로 Object.entires도 key의 타입을 스펙상 string으로 강제하고 있기 때문에 기존 객체를 as const로 선언하였더라도 한번 entries를 거쳐 나오면 string 타입으로 풀리게 됩니다 지난 타입스크립트 세션 때 같이 해봤던 d.ts 파일 읽기를 해보시면 유사한 경우가 생겼을 때 어렵지 않게 대응하실 수 있을거 같아요

// lib.es2017.object.d.ts
interface ObjectConstructor {
  entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];
}
sjyoung428 commented 2 years ago

@movie42

혹시 재현 코드에서 jsx를 리턴하지 않는데 ts가 아닌 tsx확장자로 하신 이유가 있을까요??

그리고 제네릭 타입 지정해주실 때 TData extends unknown를 하신 이유도 궁금합니다!

movie42 commented 2 years ago

@starkoora 아... 낫 놓고 ㄱ도 놀라봤네요... json(): Promise 였군요. 타입 단언으로 해결해보았습니다. 감사합니다.

movie42 commented 2 years ago

@sjyoung428 질문 해주신걸 보고 살이 떨렸습니다. 사실 왜 사용하는지 모르고 그냥 사용했거든요.

일단 jsx를 리턴하지 않는데 ts가 아닌 tsx 확장자로 만든 이유는 React Component임을 나타내기 위해서 그냥 tsx로 썼습니다. 찾아보니 ts와 tsx 차이는 pure와 jsx를 포함하는 것의 차이라고 합니다. 관련 링크

제네릭 타입 지정해줄 때 TData extends unknown을 한 이유는요... ㅜㅠ 사실 재사용 사능한 컴포넌트를 만들기 위해서 제네릭을 공부하면서 어느 블로그에 있는걸 보고 그냥 따라 만든겁니다.

그래서 찾아봤는데요. 사실 완벽하게 설명은 못하겠다가 결론입니다. 일단 정리한거 대강 올려봅니다.

  1. Typescript에서 unknown은 타입 이론에 따라 최상위 타입이라고 합니다. (반면에 never는 최하위 타입) 그래서 모든 타입은 unknown이라고 합니다.(unknown이 모든 타입이 될 수 있는건지 모든 타입이 unknown인건지... 햇갈리네요)
  2. JSX에서 확장된 TSX를 사용할 경우 \<T>가 컴파일러가 읽을 때 불분명한 부분이 있어서 컴파일러에게 Generic 타입이라는 것을 알려주기 위해서 \<T extends unknown>이라고 알려주기 위해서라고 합니다.
  3. TSX에서 사용할 때 \<T>와 \<T extends unknown>의 차이는 없다고 합니다. (1번에 따르면 수긍이 됩니다.)
  4. unknown은 Type safe를 위해서 사용한다고 합니다. (이건 질문 내용과는 관련이 없지만 그냥 unknown이 뭔지 찾다고 꼬리를 물다가 발견해서...)

위의 글을 읽고 그냥 대강 이해한 부분만 정리했습니다. 번역기로 돌려도 무슨말인지 모르겠는 것도 많네요.

멘토님께 크로스 체크를 받으면 좋을 것 같습니다. 😅

@starkoora 멘토님 혹시... 도와주실 수 있으신가요..? ㅜ

starkoora commented 2 years ago

@movie42 2, 3번 내용으로 맞게 답변 해주신 것 같습니다

첨언하자면 타입은 집합 개념으로도 이해할 수 있는데요. 이러한 관점에서 바라본다면 any / unknown은 전체집합, never는 공집합에 해당합니다.

다만 any는 모든 타입들의 상위집합이면서 모든 타입들의 부분집합이기도 합니다. 어떤 타입이든 any에 할당할 수도 있고, any를 어떤 타입에든 할당할 수 있는 것이죠.

반면 unknown은 모든 타입들의 상위집합이지만 부분집합은 아닙니다. 그래서 T extends unknown에는 어떤 것이든 들어올 수 있지만 unknown인 타입을 다른 타입에 할당할 수는 없는 것(오직 unknown과 any에만 할당 가능)입니다. 그래서 T extends unknown이 가능한 거죠.

타입스크립트 관련한 실전적인 예시들이 댄 밴더캄 - 이펙티브 타입스크립트에 나와 있으니 한번 읽어보시면 좋을거 같아요

movie42 commented 2 years ago

@starkoora 감사합니다!