Redux-Saga の CallEffect の返り値が常に any なのを typed-redux-saga は使わないでなんとかする

ふとしです。

以前は CallEffect 内で全てを完結するコードを書くことにより解決としていましたが、コードが回りくどくなりよくありません。

対処法

平易な対処法があったので紹介します。

参考: Typescript CallEffect is any · Issue #1504 · redux-saga/redux-saga

Redux-Saga の一般的な用法では yield の左辺は常に any になります。

そこでyield に渡す前の CallEffect を変数とし、左辺で CallEffect が返すことになっている型を指定します。

type EffectRT<T extends Effect> = T extends CallEffect<infer R> ? R : never;
const uploadCall = call(api.upload, { images });
const uploaded: EffectRT<typeof uploadCall> = yield uploadCall;

Redux-Saga では返されるものが確定しているので、安全にこのように型を強制できるということです。

typed-redux-saga の場合

typed-redux-saga は yield* の左辺が Generator の return 値になることを利用して型を特定しています。

ごく簡単には以下のような仕組みを包括的に提供しています。

非同期に値を返す関数

boolean を返す非同期処理があるとします。

async function getFoo(): Promise<boolean> {
  return true;
}

call をラップする関数を用意する

通常の call の結果を得て返すだけの Generator を定義します。

そこで Generator の型 Generator<T, TReturn, TNext> の第二型引数が return 値の型指定に使えるので fn の返り値から目的の型を取り出して指定します。

yield の左辺は any であることから好きな型を指定することは違法にはなりません。

function* wrapCall<Fn extends (...args: any[]) => any>(
  fn: Fn,
  ...args: Parameters<Fn>
): Generator<
  any,
  Fn extends (...args: any[]) => Promise<infer RT> ? RT : never
> {
  return yield call(fn, ...args);
}

yield* で呼ぶ

yield では依然として any しか得られませんが yield* では return に指定した型を得られるので const fooboolean 型として特定できます。

function* work() {
  // foo は boolean になる
  const foo = yield* wrapCall(getFoo);
}

このような関数と型定義を提供するのが typed-redux-saga です。

おわりに

経過を得る yield ではなく return 値を得る yield* を使うことによる解決法は目からウロコが落ちました。

typed-redux-saga があれば大規模でも型の心配をせずに安定して使えそうですね。