typescript-fsa-redux-sagaことはじめ

# [typescript-fsa](https://github.com/aikoven/typescript-fsa)しゅごい :sunflower: 

そんな便利な`typescript-fsa`には以下のCompanion Packagesが同じ作者より提供されています(リンク省略)

- `typescript-fsa-redux-saga`  
- `typescript-fsa-redux-observable`
- `typescript-fsa-redux-thunk`
- `typescript-fsa-reducers`

この記事では`typescript-fsa-redux-saga`の使い方について簡単にまとめた記事になります(一部`typescript-fsa`及び`typescript-fsa-reducers`が入ります)。

本記事執筆時におけるバージョンは以下の通りです。

```
redux@4.0.5
redux-saga@1.1.3
typescript-fsa@3.0.0
typescript-fsa-reducers@1.2.1
typescript-fsa-redux-saga@2.0.0
```

## 提供されているAPI :hugging: 

提供されているAPIはreadmeにも書かれていますが[bindAsyncAction](https://github.com/aikoven/typescript-fsa-redux-saga#bindasyncactionactioncreators-asyncactioncreators-options-bindasyncactionoptions-higherordersaga)のみです。

`bindAsyncAction`は`AsyncActionCreator`を受け取って`HigherOrderSaga`を返す関数です。

以下のような処理を行います。

1. AACを受け取る
2. 受け取った`SagaIterator`に引数paramsを渡して起動
3. `put(AAC.started({params}))`(省略可)
4. 受け取った`SagaIterator`が処理を終え`return`/`throw`すると
    - `return`の場合、`yield`したresultを`put(AAC.done({params, result}))`
    - `throw`の場合、`catch`し`throw`されたerrorを使って`put(AAC.failed({params, error}))`

### AsyncACtionCreator

`AsyncActionCreator`(AAC)は`typescript-fsa`の`actionCreator#async`で作られるオブジェクトです。
作り方等は私の記事で恐縮ですが[:poop:こちら:poop:](https://qiita.com/kentac55/items/8333dbc3f65e50400dd7#%E9%9D%9E%E5%90%8C%E6%9C%9F%E7%94%A8action%E3%82%92%E4%BD%9C%E3%82%8B-gear)を参照ください。

### HigherOrderSaga

`HigherOrderSaga`は`SagaIterator`を受け取る関数です。

`SagaIterator`は`redux-saga`にて以下のように定義されています。

```typescript
export type SagaIterator<RT = any> = Iterator<StrictEffect, RT, any>
```

`Iterator`の定義も見てみます。

```typescript
interface Iterator<T, TReturn = any, TNext = undefined> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}
```

つまり`next()`した際に`IteratorYieldResult<StrictEffect>`が返ってくるIterableオブジェクトが来れば良さそうです。

次に`StrictEffect`について見てみます。

```typescript
export type StrictEffect<T = any, P = any> = SimpleEffect<T, P> | StrictCombinatorEffect<T, P>;

export interface SimpleEffect<T, P = any> extends Effect<T, P> {
  combinator: false
}
export interface StrictCombinatorEffect<T, P> extends Effect<
  T, CombinatorEffectDescriptor<StrictEffect>
> {
  combinator: true
}

export interface Effect<T = any, P = any> {
  '@@redux-saga/IO': true
  combinator: boolean
  type: T
  payload: P
}
```

ここから以下のことが分かりました

- `StrictEffect`は`SimpleEffect`と`StrictBombinatorEffect`の共用体型ということ
- `SimpleEffect`と`StrictBombinatorEffect`は`Effect`を継承しているということ
- `Effect`はFSAを拡張したものということ

## 使ってみる :punch: 

型パワーを実感するために使ってみます。

1秒かけて`string`の文字数を返す`async`関数をsagaで動かすことを考えてみます。また与えられた文字数が3文字の場合`Error`を`throw`します。

まずはAACとその`async`関数を定義します。

```typescript
// いつもの
import actionCreatorFactory from 'typescript-fsa'
const actionCreator = actionCreatorFacory()

// AAC定義
const getLengthAction = actionCreator.async<string, number, Error>('getLengthAction')

// async関数
const getLength = async (s: string): Promise<number> => {
  await new Promise(resolve => globalThis.setTimout(resolve, 1000))
  if (s.length !== 3) {
    return s.length
  } else {
    throw new Error('error')
  }
}
```

次にsaga workerを定義します。

`typescript-fsa-redux-saga`を使わない場合は似たような処理を以下のように書くと思います。

```typescript
function* getLengthWorker(s: string) {
  try {
    const result = yield call(getLength, s)
    yield put({type: 'SomeActionDone', payload: result})
  } catch (e) {
    yield put({type: 'SomeActionFailed', payload: e, error: true})
  }
}
```

`typescript-fsa-redux-saga`を使う場合は以下のように書きます。

```typescript
// saga worker
function* getLengthWorker(s: string) {
  return yield call(getLength, s)
}

// saga workerをwrapしたworker
const boundGetLengthWorker = bindAsyncAction(getLengthAction)(getLengthWorker)
```

処理結果をreturnしていないことや例外をcatchしていないのでだいぶシンプルになっていると思います。

参考までに、`getLengthWorker`は`SimpleEffect`を継承した`CallEffect`を返す`call`を`yield`し、その結果を`return`しています。これにより`Iterator<SimpleEffect>`のシグネチャを満たしています。

なので

```typescript
function* getLengthWorker(s: string) {
  // return yield call(getLength, s)
  return yield s
}
```

のように`getLengthWorker`を書き換えると`getLengthWorker`の型は`Iterator<string>`になり`bindAsyncAction`のシグネチャを満たさずコンパイルエラーになります。

次に`boundGetLengthWorker`の起動方法を見てみます。

```typescript
function* getLengthHandler(): SagaIterator {
  yield takeEvery(getLengthAction.started, function*({payload}) {
    yield call(boundGetLengthWorker, payload)
  })
}

export function* rootSaga(): SagaIterator {
  yield fork(getLengthHandler)
}
```

`getLengthHandler`が`getLengthAction.started`を受け取ると`boundGetLengthWorker`を起動します。
`payload`は`getLengthAction.started`のpayloadの型なのでここでの型チェックを行うことができます。

以上が大まかな流れです。

## 注意点 :warning: 

### `AAC.started`の罠 :hole: 

さて、上記のサンプルには致命的なミスがあります。`getLengthHandler`は`getLengthAction.started`を受け取り`boundGetLengthWorker`を起動していますが、その`boundGetLengthWorker`内部でも`getLengthAction.started`を発火させています。
そのため**このまま実行すると無限ループに陥ってしまいます。** :scream: 

これを回避するためには以下の2つの手段が取り得ます。

- saga workerで`put(getLengthAction.started())`しない
- `takeEvery`で受け取るActionを変える

#### saga workerで`put(getLengthAction.started())`しない :skier: 

この方法は非常にスッキリ書けます。以下のように`bindAsyncAction`にoptionを渡すだけです。

```typescript
const boundGetLengthWorker = bindAsyncAction(getLengthAction, {skipStartedAction: true})(getLengthWorker)
```

#### `takeEvery`で受け取るActionを変える :snowboarder: 

この方法は書き方が冗長になりますがもう少し自由度をもたせることが可能です。
以下のようにsaga起動用のactionを別に定義します。

```typescript
// AAC定義
type getLengthProp = string
const getLengthAction = actionCreator.async<getLengthProp, number, Error>('getLengthAction')

// trigger用Action
const getLengthOp = actionCreator<getLengthProp>('GetLengthOp')

// ...

function* getLengthHandler(): SagaIterator {
  yield takeEvery(hogeAsyncOp, function*({payload}) {
    yield call(boundGetLengthWorker, payload)
  })
}
```

このようにすることで例えば`takeEvery`した後様々な前処理を行い、その後実行される`boundGetLengthWorker`が起動したタイミングを正確に`put`したいというニーズを満たすことができます(すいません、このシチュエーションは相当なレベルのコーナーケースだと思います。他に良い例があれば教えてください・・・)。

なおこの場合でも仮に`getLengthAction`のPropsの型と`getLengthOp`のPropsの型が異なった場合`getLengthHandler`にて型エラーを検知できます。

個人的には特段の事情が無い限りは`{skipStartedAction: true}`して`AAC.started`を`dispatch`するのが良いと思います。

### `AAC.failed`は型安全ではない :unlock: 

実際に使用する際は`typescript-fsa-reducers`を合わせて以下のように使うと思います。

```typescript
import { reducerWithInitialState } from 'typescript-fsa-reducers'
import { ErrorState } from '../containers/Error'

const failedHandler = (_: unknown, { error }: { error: Error }): ErrorState => {
  return {
    error: { type: error.name, message: error.message },
  }
}

export const errorReducer = reducerWithInitialState({
  error: null,
} as ErrorState)
  .case(getLengthAction.failed, failedHandler)
```

reducer内では型安全が保証されています。`getLengthAction.failed`のpayloadは`{ error: Error }`が含まれています。これは一見問題なさそうです。

しかし例えば`getLengthAction`でbindされたsaga workerで`call`しているasync関数(`getLength`)が以下のようになっている場合どうなるでしょうか。

```typescript
const getLength = async (s: string): Promise<number> => {
  throw 'a'
}
```

この場合は型エラーが出ません。javascriptでは全てのオブジェクトがthrowableなためです。
そのため丁寧にthrowしないと実行時エラーが起きてしまう危険があります。

## 終わりに :notebook: 

以上を元に`typescript-fsa-redux-saga`について簡単にまとめてみます。

- メリット
    - `try...catch...`のようなボイラープレートを削減できる
    - 型チェックできる部分が増える
- デメリット
    - saga worker内で`throw`しないといけない

個人的な所感ですが`throw`する事を許容する/しないという点が採用にあたってのポイントになると思います。
特にsaga内で`call`する非同期関数で謎のerrorをthrowされる危険を許容できないのであれば`typescript-fsa-redux-saga`を採用せず`try...catch...`で手動でAACを`put`するのも有りだと思います。

また、現状`bindAsyncAction`は`throw`された時のみ`AAC.failed`を`put`するという点も覚えておきたいところです。