LoginSignup
4
2

More than 1 year has passed since last update.

[TypeScript] Typed Redux Sagaで型安全なsagaを書く

Posted at

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.fetchUserPromise<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するように置き換えてくれます。

参考

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2