タイプスクリプトで日常的に働いている5年と私は、ジェネリックがそれをすることができたという考えが全くありませんでした🤯!



文脈
私たちが所有していないAPIは、同じプロパティを含むオブジェクトを返すことができます.例:
interface Order {
  // some unique keys
  id: string;
  name: string;

  // some keys "grouped" by 3 
  price1: number;
  price2: number;
  price3: number;

  quantity1: number;
  quantity2: number;
  quantity3: number;

  shippingDate1: string;
  shippingDate2: string;
  shippingDate3: string;
}
注意:
  • なぜそのAPIが露出していないのかprices , quantities and shippingDates 配列がポイントを超えています.修正できないAPIを消費しようとしましょう
  • これは単なる例です、そして、返されたタイプはそれのようなより多くの特性を持つことができました、したがって、我々は手動で全体の再マッピングを作らなければなりません

  • 挑戦
    バックエンドが良い理由のためにこのようにそれを公開するかもしれませんが、我々のケースでは、フロントエンド側で、我々はむしろより良い方法で我々のニーズに合っているデータ構造を持っていたいです.

    第1部
    ビルドAgetGroupedValues 持つ関数
  • 入力1:オブジェクト
  • 入力2 :繰り返しのプロパティのキー.例を渡すとOrder 最初の引数として、2番目の引数として渡すことができますprice , quantity or shippingDate
  • 出力:その一般的なもののすべてのキーの結合にマッチするタイプの配列.例Order , 2番目の引数がprice 我々は、出力として期待するnumber[] でももしshippingDate 我々は期待するstring[]
  • これは次のようなブロックを構築することができます.
    interface OrderRemap {
      id: string;
      name: string;
    
      prices: number[];
      quantities: number[];
      shippingDates: string[];
    }
    
    declare const order: Order; // no need to know where it's coming from for the example
    
    const orderRemap: OrderRemap = {
      id: order.id,
      name: order.name,
    
      prices: getGroupedValues(order, 'price'),
      quantities: getGroupedValues(order, 'quantity'),
      shippingDates: getGroupedValues(order, 'shippingDate'),
    };
    
    主な目的は:可能な限り安全なタイプとしてその機能を持っている.

    第2部
    次のすべてを処理する関数を構築しましょう.
    const orderRemap: OrderRemap = {
      id: order.id,
      name: order.name,
    
      prices: getGroupedValues(order, 'price'),
      quantities: getGroupedValues(order, 'quantity'),
      shippingDates: getGroupedValues(order, 'shippingDate'),
    };
    
    代わりに、型がこのように定義されている場合:
    interface OrderDetail {
      price: number;
      quantity: number;
      shippingDate: string;
    }
    
    interface OrderRemapGrouped {
      id: string;
      name: string;
    
      orderDetails: OrderDetail[]
    }
    
    単純に行う必要があります.
    declare const order: Order; // no need to know where it's coming from for the example
    const orderRemapGrouped: OrderRemapGrouped = groupProperties(order, 'orderDetails');
    
    あなたはこれらの課題は、解決策を読む前に自分自身を移動したい場合は、今は良い時間です!単に開くことができますhttps://www.typescriptlang.org/play そして、結局、コメントとしてあなたの遊び場のURLを共有してください😄.

    実装

    第1部
    このAPIは常にこれらのプロパティを返します.price1 , price2 , price3 ).
    そこで、最近ではtypescriptの機能を使ってここから始めます.Template literal types .
    type Indices = 1 | 2 | 3;
    
    type GroupedKeys<T> = T extends `${infer U}${Indices}` ? U : never;
    
    それは3回右に表示されるすべてのプロパティの基本キーを抽出することはかなり簡単ですか?
    テストをするなら、以下のように出力します:
    interface Order {
      id: string;
      name: string;
    
      price1: number;
      price2: number;
      price3: number;
    
      quantity1: number;
      quantity2: number;
      quantity3: number;
    
      shippingDate1: string;
      shippingDate2: string;
      shippingDate3: string;
    }
    
    type Indices = 1 | 2 | 3;
    
    type GroupedKeys<T> = T extends `${infer U}${Indices}` ? U : never;
    
    type Result = GroupedKeys<keyof Order>; // "price" | "quantity" | "shippingDate" 🎉
    
    しかし、心配しないでください、それは私が得ていたところでありません.があります.
    では、私たちのジェネリックを作りましょうgetGroupedValues 関数(ポイントとしての実装ではなく、定義)
    function getGroupedValues<Obj, BaseKey extends GroupedKeys<keyof Obj>>(object: Obj, baseKey: BaseKey): Array<Obj[`${BaseKey}${Indices}`]> {
      return null as any; // we're not implementing the function, just focusing on its definition
    }
    
    これはまさに我々が欲しいものかもしれない感じ!
  • したがって、任意のオブジェクトタイプを受け入れるObj 上記の制約はない
  • ベースキーはグループ化されたキーの1つであり、Obj , したがって、私たちは書くBaseKey extends GroupedKeys<keyof Obj>
  • 入力をタイプします(ここでは空想はありません)object: Obj, baseKey: BaseKey
  • 戻り値の型については、価格の値の配列を取得したい場合は知っていますprice1 , price2 , price3 ) でbaseKey 入力として渡されますprice . したがって、オブジェクトにアクセスするならば${BaseKey}${Indices} , 我々は得るprice1 | price2 | price3 どれが欲しいのか
  • ファンタスティック!すべてが完了!でも待ちます.活字はここで本当に幸せに思われません.返り値:
    Obj[`${BaseKey}${Indices}`]
    
    と言う.

    Type '${BaseKey}1 | ${BaseKey}2 | ${BaseKey}3' cannot be used to index type 'Obj'


    そして、それは合法的に見えます.我々は、何も拡張しないジェネリックのプロパティにアクセスしようとしていますObj 種類).
    しかし、どのように我々はこの機能を一般的に保つことができて、我々のオブジェクトがベースキーとインデックスから成るキーを持つと指定することができますか🤔...
    Would
    Obj extends Record<`${BaseKey}${Indices}`, any>
    
    仕事?確かにそれは動作できませんBaseKey の後に定義されObj :
    Obj extends Record<`${BaseKey}${Indices}`, any>, BaseKey extends GroupedKeys<keyof Obj>
    
    よく、これはtypescriptのためにちょうどよいです🤯.
    再びこれを読んでください.
    与えられたタイプと言うのは良いことだObj オブジェクトになります(Record ) これは他の型のキーを含むBaseKey ), そのキーを読むことによって定義されるObj .
    ミー
    私は、タイプスクリプトがリストを持つことができるリストを定義するかどうか、リストを持つことができるように、ちょうどあなたが再帰的に再帰を扱うことができるということを知っていました.
    interface List {
      propA: number;
      list?: List
    }
    
    const list: List = {
      propA: 1,
      list: {
        propA: 2,
        list: {
          propA: 3
        }
      }
    }
    
    でもこれ?私はそれが驚くべきだと思う間、タイプスクリプトがどのようにタイプを解決するかを確信していません.
    これをラップするには、完全な機能があります(実装がなければすべての型があります).
    function getGroupedValues<Obj extends Record<`${BaseKey}${Indices}`, any>, BaseKey extends GroupedKeys<keyof Obj>>(
      object: Obj,
      baseKey: BaseKey,
    ): Array<Obj[`${BaseKey}${Indices}`]> {
      return null as any; // we're not implementing the function, just focusing on its definition
    }
    
    試してみると以下のようになります.
    // some mock for an order
    const order: Order = {
      id: 'order-1',
      name: 'Order 1',
    
      price1: 10,
      price2: 20,
      price3: 30,
    
      quantity1: 100,
      quantity2: 200,
      quantity3: 300,
    
      shippingDate1: '10 Oct',
      shippingDate2: '11 Oct',
      shippingDate3: '12 Oct',
    }
    
    const orderRemap: OrderRemap = {
      id: order.id,
      name: order.name,
    
      prices: getGroupedValues(order, 'price') // inferred return type: `number[]`
      quantities: getGroupedValues(order, 'quantity') // inferred return type: `number[]`
      shippingDates: getGroupedValues(order, 'shippingDate') // inferred return type: `string[]`
    };
    
    私は、ちょうどエラーでしばらく立ち往生した後、多くの望みなしで金曜日の午後にこのトリックをためしました

    Type '${BaseKey}1 | ${BaseKey}2 | ${BaseKey}3' cannot be used to index type 'Obj'


    私は、どうにか、タイプスクリプトが同じレベルで定義されて、互いを利用しているそれらのジェネリックでOKであるのを見て驚きました
    Obj extends Record<`${BaseKey}${Indices}`, any>, BaseKey extends GroupedKeys<keyof Obj>
    
    私は唯一のものですか?ご存知ですか?誰でも、タイプスクリプトがこれでOKでありえる方法を説明することができますか?いずれにせよ、コメントを残して、あなたがそれについてどう思うか、そして、これが役に立つならば、私に話してください😄!
    ここではTypescript Playground link 上記からのすべてのコードで.

    第2部
    パート1でAPIを作成している間、我々はgetGroupedValues 関数を複数回、キーを渡し、残りのプロパティなどのオブジェクト全体を再作成します.簡単に!
    そこで、どのようにしてこれらをすべての関数として作成し、そのインデックスに基づいて異なるプロパティをグループ化するかを見ることができます.
    const orderRemapGrouped: OrderRemapGrouped = groupProperties(order, 'orderDetails');
    
    だからここで.orderDetails 型のオブジェクトを含む配列です{price: number, quantity: number; shippingDate: string} 値が同じインデックスから来る場合.例orderDetails[0] , それにはprice , quantity and shippingDate of price1 , quantity1 and shippingDate1 . など
    function groupProperties<Obj extends Record<`${Keys}${Indices}`, any>, Keys extends GroupedKeys<keyof Obj>, NewKey extends string>(
      object: Obj,
      newKey: NewKey,
    ): Omit<Obj, `${Keys}${Indices}`> & Record<NewKey, Array<{[key in Keys]: Obj[`${key}${Indices}`]}>> {
      return null as any; // we're not implementing the function, just focusing on its definition
    }
    
    ここでいくつかの類似点を参照してください?
    正確に!最大の違いは、戻り型です.それで、それを壊しましょう
    Omit<Obj, `${Keys}${Indices}`>
    
    まず最初に、返される新しいオブジェクトがインデックスを持つプロパティのいずれかを持っていないことを知っています.price1 , price2 , price3 ). それで、我々はビルトインOmit インデックスに連結された共通キーから除外する型.
    Then:
    Record<NewKey, Array<{[key in Keys]: Obj[`${key}${Indices}`]}>>
    
    戻り値の1つのプロパティに追加されます.この関数は、NewKey ). そのキーには、オブジェクトの配列になる値があります.
    これらのオブジェクトはすべて共通のキーになりますprice , quantity and shippingDate ), すべてのこれらのプロパティのunion型に関連付けられます.例えば、price 我々は、ユニオンのタイプを取得しますprice1 , price2 , price3number 彼らはすべての数字として.と他の同じ.
    ここではTypescript Playground link 上記からのすべてのコードで.

    結論
    私は、オブジェクトとそれのキー(パート1)の間で2つのジェネリックを参照することが働くと思いませんでした.しかし、それは最高です.いくつかのプロパティの一般的なビットを抽出するために、そのテンプレートとそのトリックの組み合わせは、非常に強力であり、私たちの関数には、かなり異なる別のデータ構造を変更するための堅牢なタイピングを与えます.
    私は、ちょうどタイプスクリプトが好きです❤️✨. 同様にしてください、そして、あなたが役に立つこの記事を見つけたならば、コメントで知らせてください.また、私は本当にそれを試みた人々から若干の試み/解決を見ることに興味があります!

    タイポを発見?
    あなたがtypo、改善されることができた文またはこのブログ柱で更新されるべき何かを見つけたならば、あなたはそれをGITリポジトリでそれにアクセスして、プル要求をすることができます.コメントを投稿するのではなく、直接あなたの変更で新しいプル要求を行ってください.