📘

大枠の型は決めたいけど、詳細の型は宣言したものにしたい時

2021/10/08に公開

始めに

型を設定するにあたって以下のように書くことがあると思います。例としてvuexのmutationsを定義するコードで、変数側で期待する型を設定することで中身の型が推論されて便利ですよね。

変数に型を設定して推論するパターン
// vuexのmutationsの型定義
type Mutation<S> = (state: S, payload?: any) => any;
interface MutationTree<S> {
  [key: string]: Mutation<S>;
}

type State = {
  count: number;
}

const mutations: MutationTree<State> = {
  // stateが推論される
  add(state, value: number) {
    state.count += value;
  },
  sub(state, value: number) {
    state.count -= value;
  }
};

ただこれが少し問題になることが合って、例えば実際に定義されたmutationNameを取り出そうとしても取り出すことができません。MutationTreeで丸められてしまったことで、詳細の型が分からなくなってしまいます。

変数に型を書くのを止めると宣言した内容の型は付きますが、stateなどの推論が効かなくなってしまいます。

変数に型を書かないパターン
const mutations = {
  // stateの推論が出来ないので一々型をセットする必要がある
  // また、MutationTree<State>の型に収まるか分からない
  add(state: State, value: number) {
    state.count += value;
  },
  sub(state: State, value: number) {
    state.count -= value;
  }
};

// 'add' | 'sub' と詳細の型は出るが。。
type mutationName = keyof typeof mutations;

このように最低限満たしてほしい型の定義をしたいが、最終的に得る型は宣言した内容にしたいというケースはどうしたら良いか、一つの解法が出たのでそれを記事にしたいと思います。

解決方法

結論を言うと、型チェックをするためのメソッドを経由させることで上記の問題は解決します。
外側のメソッドで期待する型を決めて呼び出し、内側のメソッドでは期待した型を満たしているか確認しつつ、そこで宣言された内容をただ返すようにしています。

型チェック用のメソッドを用意していいとこ取りをする
function typeChecker<T>() {
  return function check<U extends T>(checkVar: U) {
    return checkVar;
  };
}

const mutations = typeChecker<MutationTree<State>>()({
  // 推論もちゃんと効く
  add(state, value: number) {
    state.count += value;
  },
  sub(state, value: number) {
    state.count -= value;
  }
});

// ちゃんと 'add' | 'sub' が出てくる
type mutationName = keyof typeof mutations;

終わりに

以上が大枠の型を決めつつ、宣言した型が入るようにする方法でした。昔から割と悩んでいて、どうしようもないのかなと諦めていましたが、今回割といい解法が出たんじゃないかなと思います。ただカリー化して2回メソッドを呼ぶ必要があるのが少し気になっているので、他にもっと良い方法があればコメントしてくれると嬉しいです。

サンプルコードをplaygroundに書きましたので、コードや推論状況を見たい方はこちらをご参照ください。

https://www.typescriptlang.org/play?#code/C4TwDgpgBAsgrsAhsAlgewHYB4DKA+KAXigAoBnJYCALihwBooxEQAbNRAEwH5bEMQASiIF+IANwAoFBioAnAGaIAxtHiV0GACpyIEXAQDekqFADaAawghaFOTIDmAXVrrkmg1IC+kyQrgYyqiYUKCQAMIAFhDKVnJYWngkwsamusBwchhQ-oHB2crRsVgAqlAQAB5UGJxkUIkkhTEWAGqIcrQlKSamUOmZBUWt7VKmXt6+YdA4lNDEqVDKaAHAtBhwALYARhByE5JLGBRQGwjumHXEU1HNu1hu+Tp6uLN4SYIkC1yc5LOMAG6IVhwGhQdbbXbdXpQCjICAAOiWKygAGpiIDgRBRlAvPQemQ4FtfnCAUCQWtNjs5FDerCqIjlrIoABadFkrE9HxeQRSSRTE5nfIAOUQGzmUCsIDQClC4Ag0oFGguUiAA

Discussion