ウェブサイト検索

ドット記法によるTypeScriptのPick


ドット表記を使ってネストされたオブジェクトの要素をPickすることは可能ですか?

interface Test {
   customer: {
      email: string;
      name: {
         firstName: string;
      };
   };
};

type PickedTest = PickByDotNotation<Test, "customer.name.firstName">;

`PickedTest` の型は `{ customer: { name: { firstName: string } } };` に等しい。

私の必要は、以下のように複数のパスをサポートする汎用的なものを持つことです。

type PickedTest = PickByDotNotation<Test, "customer.name.firstName" | "customer.email>;

回答 #1

良さそうなコーディング面接の質問ですね ;) これは、かなり前に導入されたテンプレートリテラル型を使うことで可能です。方法は以下の通りです:

type PickByDotNotation<TObject, TPath extends string> =
  // If TKey has a dot, split it into two: the part before the dot and after the dot
  TPath extends `${infer TKey}.${infer TRest}` ?
    // Checking if the key actually exists in the object
    TKey extends keyof TObject ?
      // Get type recursively
      PickByDotNotation<TObject[TKey], TRest> :
      // Provided key is invalid
      never :
  // The path doesn't contain a dot, so just trying to use it as a key
  TPath extends keyof TObject ?
    TObject[TPath] :
    never

遊び場リンク

TS 4.7で導入されたinfer ... extends ...を使うと、最初の節を少し簡単にできます

type PickByDotNotation<TObject, TPath extends string> = 
    // Constraining TKey so we don't need to check if its keyof TObject
    TPath extends `${infer TKey extends keyof TObject & string}.${infer TRest}` ?
        PickByDotNotation<TObject[TKey], TRest> :
    TPath extends keyof TObject ?
        TObject[TPath] :
        never

遊び場リンク

しかし、実際のコードでこのタイプを使用することはおすすめしません。少なくとも、常に意図した通りに動作することを保証できません。特にテンプレート文字列の部分については、TPath に複数のドットがある場合、TS のドキュメントでは TKey が最初のドットの前の部分になるのか、それとも最後のドットの前の部分になるのか、あるいは途中のランダムなドットの部分になるのかは記載されていないと私はかなり確信しています。

回答 #2

この質問は、Pick(結果が選択されたプロパティのみを含むオブジェクトのスーパタイプである場合)について尋ねており、インデックス指定(結果がパス上のプロパティの値の型である場合)についてではないので、ここに実際にPickを行う答えを置いておきます。

type PickByDotNotation<T, K extends string> = {
    [P in keyof T as P extends (K extends `${infer K0}.${string}` ? K0 : K) ? P : never]:
    P extends K ? T[P] : 
      PickByDotNotation<T[P], K extends `${Exclude<P, symbol>}.${infer R}` ? R : never>
} & {} 

あなたの例で試してみましょう:

interface Test {
    customer: {
        email: string;
        name: {
            firstName: string;
        };
    };
    anotherProp: number;
};

type PickedTest = PickByDotNotation<Test, "customer.name.firstName">;
/* type PickedTest = {
    customer: {
        name: {
            firstName: string;
        };
    };
} */

type PickedTest2 = PickByDotNotation<Test, "customer.name.firstName" | "customer.email">;
/* type PickedTest2 = {
    customer: {
        email: string;
        name: {
            firstName: string;
        };
    };
} */

いい感じです!

コードへのプレイグラウンドリンク

注:必要であれば、ここに戻ってどのように機能するかを詳しく説明することもできますが、今は単に質問された通りに答えがあることを確認したいだけです。

関連記事