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 foo
は boolean
型として特定できます。
function* work() {
// foo は boolean になる
const foo = yield* wrapCall(getFoo);
}
このような関数と型定義を提供するのが typed-redux-saga です。
おわりに
経過を得る yield
ではなく return 値を得る yield*
を使うことによる解決法は目からウロコが落ちました。
typed-redux-saga があれば大規模でも型の心配をせずに安定して使えそうですね。