タイプスクリプト:タイプ狭まりの話


ある日、あなたは2つのAPIからのデータをプルするか、さらに1つの小さな画面上でそれらをレンダリングするには、同じ時間でより多くの.データの構造は、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}</>));
}
それは完全に動作します.事態は晴れる.しかし、スクリプトは叫んで起動し、コードをコンパイルするのを防ぎます.

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


タイプスクリプトを満足させるために、私は私のコードをこれに直します
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.


残酷だが合理的!
OK、もう一度試してみましょう.私はインターフェイスを小さい部分に分けます.それはきちんとしたように見えます? , でも.
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

  • 型述語の使用
    このメソッドを使用すると、私はどんな種類のインターフェイスを検出するかを検出するユーティリティ機能を追加することができます.コードはこのようになります.
    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コードサイズを少しより大きくするということです.あなたは、タイプスクリプト遊び場の上で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 このスタイルで.一方、他のインターフェイスを再利用したい場合は、共通のプロパティを省略するか、手動でデータコレクションに追加する必要があります.そうでなければ、もう一度あなたに叫んでください.
    ここに私が話していることがあります.
    // 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はタイプチェックについてほとんど私のチームの懸念をカバーすることができます、そして、誰でも簡単にコードを理解することができます.
    あなたが他の解決策を持っているならば、下記をコメントしてください.
    読書ありがとう