TypeScript: 유형을 좁히는 이야기

작은 화면 하나에 렌더링하기 위해 2-3개 이상의 API에서 동시에 데이터를 가져와야 하는 날이 있습니다. 각 API의 데이터 구조는 약간에서 완전히 다릅니다. 백엔드 개발자가 반환된 데이터가 동일하게 보이도록 코드를 리팩터링하도록 강제할 수는 없습니다.

어떻게 하시겠습니까???

첫 번째 시도는 API의 데이터를 공통 형식으로 다시 매핑하는 것입니다. 그런 다음 그 데이터를 통합하는 것이 정말 어렵다는 것을 깨달았습니다. 그래서 나는이 코드를 생각해 냈습니다.

type SomeKindOfInterfaceHere = { hello: string };
type AnotherInterface = { world: boolean };

interface MappedDataFromApi {
  id: string | number;
  data: string[] | SomeKindOfInterfaceHere | AnotherInterface;
}

function AReactComponent(props: MappedDataFromApi) {
  if (props.data.hello) {
    return <>display {props.data.hello} </>
  }

  if (props.data.world) {
    return <>display {props.data.world} </>
  }

  return props.data.map(d => (<>display array item: {d}</>));
}


그것은 완벽하게 작동합니다. 잘 렌더링됩니다. 그러나 Typescript는 고함을 지르기 시작하고 내가 코드를 컴파일하는 것을 방해합니다.

Property ‘hello’ does not exist on type >‘SomeKindOfInterfaceHere | AnotherInterface | string[]’.
Property ‘hello’ does not exist on type ‘AnotherInterface’.(2339)



Typescript를 만족시키기 위해 내 코드를 다음으로 리팩토링합니다.

interface MappedDataFromApi {
  id: string | number;
  contentVR?: SomeKindOfInterfaceHere;
  documentsInfo?: string[];
  bundleInfo?: AnotherInterface;
}

function AReactComponent(props: MappedDataFromApi) {
  if (props.contentVR) {
    return <>display {props.contentVR.hello} </>
  }

  if (props.bundleInfo) {
    return <>display {props.bundleInfo.world} </>
  }

  return props.documentsInfo && props.documentsInfo.map(d => (<>display array item: {d}</>));
}


물론 Typescript는 이제 더 나은 느낌을 받을 수 있습니다. 우리는 또 다른 문제를 만들었습니다. 일부는 다음과 같이 말할 수 있습니다.

Hey mate, this interface is bad. It has so many ? It is arbitrary. How can I re-use it elsewhere? How can I maintain your code once you not here? Those properties look mysterious.



잔인하지만 합리적인 소리!

알겠습니다. 다시 한 번 시도해 보겠습니다. 내 인터페이스를 더 작은 조각으로 나눕니다. 깔끔해 보이긴 하지만?...

interface VerificationRequest {
  uuid: string;
  content: SomeKindOfInterfaceHere;
}

interface SingleVerification {
  id: number;
  documents: string[];
}

interface Bundle {
  id: number;
  info: AnotherInterface;
}

type MappedDataFromApi = VerificationRequest | SingleVerification | Bundle;

function AReactComponent(props: MappedDataFromApi) {
  if (props.content) {
    return <>display {props.content.hello} </>
  }

  if (props.info) {
    return <>display {props.info.world} </>
  }

  return props.documents.map(d => (<>display array item: {d}</>));
}


Brrrrr, Typescript가 이전과 동일한 문제로 다시 나에게 고함을 지릅니다.

Property ‘content’ does not exist on type ‘MappedDataFromApi’.
Property ‘content’ does not exist on type ‘SingleVerification’.(2339)



운 좋게도 Typescript에는 더 나은 코드를 작성하고 이 경우 좋은 타이핑을 할 수 있는 이러한 보석이 있습니다.
  • Using type predicates
  • Discriminated unions

  • 유형 술어 사용



    이 방법을 사용하면 Typescript가 내가 작업 중인 인터페이스의 종류를 감지하도록 지원하는 몇 가지 유틸리티 기능을 추가할 수 있습니다. 코드는 다음과 같습니다.

    function isVerificationRequest(props: MappedDataFromApi): props is VerificationRequest {
      return !!(props as VerificationRequest).content;
    }
    
    function isSingleVerification(props: MappedDataFromApi): props is SingleVerification {
      return Array.isArray((props as SingleVerification).documents);
    }
    
    function isBundle(props: MappedDataFromApi): props is Bundle {
      return !!(props as Bundle).info;
    }
    
    function AReactComponent(props: MappedDataFromApi) {
      if (isVerificationRequest(props)) {
        return <>display {props.content.hello} </>
      }
    
      if (isBundle(props)) {
        return <>display {props.info.world} </>
      }
    
      return props.documents.map(d => (<>display array item: {d}</>));
    }
    


    아름답죠? 👏👏👏

    한 가지는 이 스타일이 최종 JS 코드 크기를 조금 더 크게 만든다는 것입니다. Typescript Playground에서 JS 컴파일 버전을 확인할 수 있습니다.

    차별적 노동조합



    이 방법을 사용하면 리터럴 유형의 공통 속성을 인터페이스에 추가할 수 있습니다. 코드는 다음과 같습니다.

    interface VerificationRequest {
      uuid: string;
      content: SomeKindOfInterfaceHere;
      kind: 'verification-request';
    }
    
    interface SingleVerification {
      id: number;
      documents: string[];
      kind: 'single-verification';
    }
    
    interface Bundle {
      id: number;
      info: AnotherInterface;
      kind: 'bundle';
    }
    
    type MappedDataFromApi = VerificationRequest | SingleVerification | Bundle;
    
    function AReactComponent(props: MappedDataFromApi) {
      switch (props.kind) {
        case 'verification-request':
          return <>display {props.content.hello} </>
        case 'single-verification': 
          return props.documents.map(d => (<>display array item: {d}</>));
        case 'bundle':
          return <>display {props.info.world} </>
        default:
          return null;
      }
    }
    


    보기에도 깔끔합니다. 이 스타일로 Exhaustiveness checking을 만들 수도 있습니다. 그러나 다른 한편으로 인터페이스를 다른 곳에서 재사용하려면 공통 속성을 생략하거나 데이터 컬렉션에 수동으로 추가해야 합니다. 그렇지 않으면 Typescript가 다시 한 번 소리를 지릅니다.

    내가 말하는 내용은 다음과 같습니다.

    // drop "kind" by create a new Omit type
    type NewSingleVerification = Omit<SingleVerification, "kind">
    function getSingleVerification(): NewSingleVerification {
      return {
        id: 1,
        documents: ['education', 'license'],
      };
    }
    
    // OR
    function getSingleVerification(): SingleVerification {
      return {
        id: 1,
        documents: ['education', 'license'],
    
        // manual add this
        kind: 'single-verification',  
      };
    }
    


    이것은 UI 문제가 비즈니스 로직에 관련되어서는 안 되는 부분을 포함하기 때문에 제게는 큰 단점입니다.

    결론



    이것들은 내가 생각해낼 수 있는 모든 해결책입니다. 각각의 단점이 있지만 적어도 마지막 2개는 유형 검사에 대한 거의 우리 팀의 우려를 다룰 수 있고 모든 사람이 코드를 쉽게 이해할 수 있습니다.
    다른 해결 방법이 있으면 아래에 의견을 보내주십시오.


    읽어주셔서 감사합니다

    좋은 웹페이지 즐겨찾기