takefumi-yoshii/ts-nuxtjs-express

Vuex の Getters の型定義

inouetakuya opened this issue · 3 comments

素晴らしいサンプルをありがとうございます!

Vuex の Getters の型について意図したとおりに型推論されないケースがありましたので質問させてください。

知りたいこと

  • state のみを引数にもつゲッター
  • state の他に getters も引数として扱うゲッター

この 2つを両立させるために Getters の型定義をどう書くべきか知りたい。

以下、このリポジトリのコードを題材にして、説明させてください。

Step 1

まず /types/vuex/type.ts の Getters の型について

type Getters<S, G> = {
[K in keyof G]: (
state: S,
getters: G,
rootState: RootState,
rootGetters: RootGetters
) => G[K]
}

type Getters<S, G> = {
  [K in keyof G]: (
    state: S,
    getters: G,
    rootState: RootState,
    rootGetters: RootGetters
  ) => G[K]
}

ゲッターの引数として、state 以外はオプショナルなので、これは誤りなのではないかと考えました。正しくは下記かと。

type Getters<S, G> = {
  [K in keyof G]: (
    state: S,
    getters?: G,
    rootState?: RootState,
    rootGetters?: RootGetters
  ) => G[K]
}

下記のテストコード(jest)を書いて検証してみましたが、これは TypeScript の型チェックエラーによってこけます。

import { state as initialState, getters } from '~/store/todos'

describe('todos module', () => {
  describe('getters', () => {
    describe('doneCount', () => {
      test('works', () => {
        const state = initialState()

        // TS2554 Expected 4 arguments, but got 1.
        expect(getters.doneCount(state)).toBe(0)
      })
    })
  })
})

image

$ yarn test --verbose
yarn run v1.17.3
$ jest --verbose
 FAIL  __tests__/store/todos/index.test.ts
  ● Test suite failed to run

    TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
    __tests__/store/todos/index.test.ts:8:16 - error TS2554: Expected 4 arguments, but got 1.

    8         expect(getters.doneCount(state)).toBe(0)
                     ~~~~~~~~~~~~~~~~~~~~~~~~

      types/vuex/type.ts:9:7
        9       getters: G,
                ~~~~~~~~~~
        An argument for 'getters' was not provided.

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.439s, estimated 2s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Step 2

しかしながら、Getters の型を下記のように修正した後では、

type Getters<S, G> = {
  [K in keyof G]: (
    state: S,
    getters?: G,
    rootState?: RootState,
    rootGetters?: RootGetters
  ) => G[K]
}

今度はゲッターの中で getters を使うコードが TypeScript の型チェックエラーになります。

export const getters: Getters<S, G> = {
  todosCount(state, getters, rootState, rootgetters) {
    // TS2532: Object is possibly 'undefined'
    const dummy = getters.doneCount

    return state.todos.length + dummy - dummy
  },
  doneCount(state) {
    return state.todos.filter(todo => todo.done).length
  }
}

image

まとめ

というわけで、

  • state のみを引数にもつゲッター
  • state の他に getters も引数として扱うゲッター

を両立させるには、どのように型を定義するのがよいか分からず、質問するに至りました。

お忙しいところ恐縮ですが、ご確認のほどよろしくお願いします。

再現手順

https://github.com/inouetakuya/ts-nuxtjs-express/tree/ts-errors-example で確認してみました

git clone git@github.com:inouetakuya/ts-nuxtjs-express.git
cd ts-nuxtjs-express
yarn install
yarn test

ファイル差分が見られる PR: Vuex の Getters の型チェックエラーの例 by inouetakuya · Pull Request #1 · inouetakuya/ts-nuxtjs-express
takuya/ts-nuxtjs-express/types/vuex/type.ts

ご丁寧な再現状況をもってご指摘頂き、ありがとうございました。
問題を把握することができました。

本サンプルでは、Store の構成要素としてconst gettersを取り扱う前提で話を進めておりました。
掲示頂きましたとおり、純粋な関数として getter関数をテストする場合、この問題に直面します。

state 以外はオプショナルではないのか?

ゲッターの引数として、state 以外はオプショナルなので、これは誤りなのではないかと考えました。

getters に定義された関数は、store として instantiate されたのち、
各関数は引数利用有無に関わらず、自動的に第4引数まで参照を与えられます。
その観点からすると、オプショナルではありません。

これはちょうど、Array.prototype.map()で説明することができます。例えば
[1,2,3].map((value, index) => value * index)は、indexへの参照を持ちますが、
[1,2,3].map(value => value)も誤りではなく、不要な参照は省略することができます。

const getters内に定義している各関数はそれぞれ上記の
(value, index) => value * index
value => value
に相当するといえます。

解決方法

export const getters: Getters<S, G>のように、
型注釈をconst gettersに対して一律で付与していたことが原因です。
サンプルで利用しているGetters<S, G>利用せず
次のとおりに各関数引数個別に指定をすることで、解決することができます。

import { Mutations, Actions, RootState, RootGetters } from 'vuex'
import { S, G, M, A } from './type'
// ______________________________________________________
//
export const getters = {
  todosCount(
    state: S,
    getters: G,
    rootState: RootState,
    rootgetters: RootGetters
  ) {
    return state.todos.length
  },
  doneCount(state: S) {
    return state.todos.filter(todo => todo.done).length
  }
}
getters.doneCount({todos:[]}) // No Error

getters に定義された関数は、store として instantiate されたのち、
各関数は引数利用有無に関わらず、自動的に第4引数まで参照を与えられます。

なるほど、下記あたりですね。

https://github.com/vuejs/vuex/blob/91f3e69ed9e290cf91f8885c6d5ae2c97fa7ab81/src/store.js#L461-L468

store._wrappedGetters[type] = function wrappedGetter(store) {
  return rawGetter(
    local.state, // local state
    local.getters, // local getters
    store.state, // root state
    store.getters // root getters
  )
}

その観点からすると、オプショナルではありません。

そうか、たしかに。

解決方法

うんうん、頭が整理されました!

  • Store 経由で getters を呼び出す場合と、純粋な関数として getter 関数を呼び出す場合とで区別
  • 純粋な関数として getter 関数を呼び出す場合を考慮して、各 getter 関数に個別で引数の型を指定する

納得です!!回答ありがとうございました!!!

解決したようでなによりです!これにて close とさせて頂きます。