mosya
mosya Business はこちら

mosya<TC> - チェイナブルなオブジェクトに型をつけようの解説

この記事はmosya<TC>の問題の一つであるChainable型の解説になります。

問題

JavaScript では、チェイン可能なオプションがよく使われます。しかし、TypeScript に切り替えたとき、正しく型を付けることができますか?

この課題では、オブジェクトでもクラスでも何でもいいので、 option(key, value)get() の 2 つの関数を提供する型を定義してください。option では、与えられたキーと値を使って現在の config の型を拡張できます。最終的な結果は get で取得することにしましょう。

例えば

declare const config: Chainable;

const result = config
  .option("foo", 123)
  .option("name", "type-challenges")
  .option("bar", {
    value: "Hello World",
  })
  .get();

// expect the type of result to be:
interface Result {
  foo: number;
  name: string;
  bar: {
    value: string;
  };
}

この問題を解くために js/ts のロジックを書く必要はありません。型レベルのロジックだけを書いてください。

keystring のみを受け付け、value は任意の型を受け付けると仮定しても構いません。同じ key が 2 回渡されることはありません。

解答例

type Chainable<T = {}> = {
  option: <K extends string, V>(
    key: K extends keyof T ? never : K,
    value: V
  ) => Chainable<
    Omit<T, K> & Record<K, V>
  >;
  get: () => T;
};

Chainable型は、optiongetの2つのプロパティを持つオブジェクト型です。
まずはそれを前提に、optiongetの型を定義していきます。
Tには初期値として空のオブジェクト型を指定します。

type Chainable<T = {}> = {
  option: ...
  get: () => ...
}

optionは、keyvalueを引数に取り、Chainable型を返す関数です。
keystring型のみを受け付けるので、K extends stringという条件を指定します。
valueは任意の型を受け付けるので、Vという型引数を指定します。

type Chainable<T = {}> = {
  option: <K extends string, V>(key: K, value: V) => ...
  get: () => ...
}

ここで、optionメソッドで同じキーに対して値が2回渡されることを防ぐために、以下のようにkeyに対して制限します。

type Chainable<T = {}> = {
  option: <K extends string, V>(key: K extends keyof T ? never : K, value: V) => ...
  get: () => ...
}

このようにkeyがすでにTのキーに含まれている場合は、neverを返すようにして、keyが重複しないように保証します。

次に、optionメソッドの返り値の型を定義します。

type Chainable<T = {}> = {
  option: <K extends string, V>(key: K extends keyof T ? never : K, value: V) => Chainable<T & Record<K, V>>
  get: () => ...
}

Record<K, V>は、KVを受け取り、Kをキー、Vを値とするオブジェクト型を返す型です。
オブジェクトTに今回optionメソッドで渡されたkeyvalueを追加した新しい型を返すようにします。

最後に、getメソッドの返り値の型を定義します。

type Chainable<T = {}> = {
  option: <K extends string, V>(
    key: K extends keyof T ? never : K,
    value: V
  ) => Chainable<T & Record<K, V>>;
  get: () => T;
};

getメソッドは、Tを返せばいいので、get: () => Tという型を指定します。

Authored by

筆者の写真

Godai@steelydylan

Webサービスを作るのが好きなWebエンジニア。子供が産まれたことをきっかけに独立し法人化。サービス開発が大好き。
好きな言語はTypeScript。

ReactやTypeScriptなどの周辺技術が学べる
オンライン学習サービスを作りました!

詳しくはこちら
mosya

mosyaはオンラインでHTML,CSS,JavaScriptを基本から学習できるサービスです。現役エンジニアが作成した豊富なカリキュラムに沿って学習を進めましょう。

© 2023 - mosya. All rights reserved.