Redux-Sagaのcallで型推論が効かない
例えばこのようにユーザーを取得するようなsagaを作ることを考えます。
(例では前提としてRedux Toolkitを使っています)
import { call, put, takeEvery } from 'redux-saga/effects'
import { createAction } from "@reduxjs/toolkit";
import { UserApiClient } from '@api/user'
import { usersActions } from '@redux/modules/users'
const actions = {
fetchUser: "users/fetchUser",
} as const;
const actionCreators = {
fetchUser: createAction<{ userId: string }>(
actions.fetchUser
),
}
function* fetchUser(action: ReturnType<typeof actionCreators.fetchUser>) {
try {
const user = yield call(UserApiClient.fetchUser, action.payload);
// user has type "any"
yield put(usersActions.success(user));
} catch (e) {
yield put(usersActions.failure(e.message));
}
}
function* usersSagas() {
yield takeEvery(usersActions.fetchUser, fetchUser);
}
UserApiClient.fetchUser
が Promise<User>
型を返す関数の場合、call関数で非同期実行するとuserに代入されるのは User
型の値となりますが、TypeScriptではyield式の左辺はany型になってしまいます。
そのためこの場合、型アサーションで const user: User = yield call(UserApiClient.fetchUser, action.payload)
としてやる必要があります。
また同様に、並行して複数のEffectを実行するall関数もany型で返ってきます。
Typed Redux Sagaを使う
Typed Redux SagaというRedux-SagaのWrapperがあります。
これを使うと先ほどの例はこう書けます。
- import { call, put, takeEvery } from 'redux-saga/effects'
+ import { call, put, takeEvery } from "typed-redux-saga";
function* fetchUser(action: ReturnType<typeof actionCreators.fetchUser>) {
try {
const user = yield* call(UserApiClient.fetchUser, action.payload);
// user has type "User"!
yield* put(usersActions.success(user));
} catch (e) {
yield* put(usersActions.failure(e.message));
}
}
function* usersSagas() {
yield* takeEvery(usersActions.fetchUser, fetchUser);
}
redux-saga/effects
の代わりに typed-redux-saga
が提供している関数を使います。
また、 yield call()
ではなく yield* call()
と書きます。
こうすることで型をアサーションしなくても返り値を推論してくれるようになります。
なぜ型推論ができるようになるのか
yield
はanyを返すのに対し、 yield*
は指定したジェネレータの返り値を推論してくれます。
function* getCount() {
const count: number = yield 1;
return count;
}
function* testGenerator() {
// 'yield' expression implicitly results in an 'any' type because its containing generator lacks a return-type annotation.
// countAny is any
const countAny = yield 1;
// countNumber is number
const countNumber = yield* getCount();
}
typed-redux-saga
のcallはジェネレータで、redux-saga/effects
のcallをwrapしているだけです。
import { call as rawCall } from "redux-saga/effects";
export function* call(...args) {
return yield rawCall(...args);
}
Typed Redux Sagaに置き換える
Redux-SagaのEffectsで書いていた部分をTyped Redux Sagaに置き換えたい場合、こちらのESLint Pluginを使うと便利です。
READMEにある通りルールを追加することで、 redux-saga/effects
を使っていると警告してくれます。
さらにautofixで自動的に typed-redux-saga
からimportするように置き換えてくれます。