Code Monkey home page Code Monkey logo

typed-redux-saga's Introduction

Typed Redux Saga

npm Build Status Type Coverage codecov Snyk Vulnerabilities for GitHub Repo

An attempt to bring better TypeScript typing to redux-saga.

Requires TypeScript 3.6 or later.

Installation

# yarn
yarn add typed-redux-saga

# npm
npm install typed-redux-saga

Usage

Let's take the example from https://redux-saga.js.org/#sagasjs

Before

import { call, all } from "redux-saga/effects";
// Let's assume Api.fetchUser() returns Promise<User>
// Api.fetchConfig1/fetchConfig2 returns Promise<Config1>, Promise<Config2>
import Api from "...";

function* fetchUser(action) {
  // `user` has type any
  const user = yield call(Api.fetchUser, action.payload.userId);
  ...
}

function* fetchConfig() {}
  // `result` has type any
  const result = yield all({
    api1: call(Api.fetchConfig1),
    api2: call(Api.fetchConfig2),
  });
  ...
}

After

// Note we import `call` from typed-redux-saga
import { call, all } from "typed-redux-saga";
// Let's assume Api.fetchUser() returns Promise<User>
// Api.fetchConfig1/fetchConfig2 returns Promise<Config1>, Promise<Config2>
import Api from "...";

function* fetchUser(action) {
  // Note yield is replaced with yield*
  // `user` now has type User, not any!
  const user = yield* call(Api.fetchUser, action.payload.userId);
  ...
}

function* fetchConfig() {}
  // Note yield is replaced with yield*
  // `result` now has type {api1: Config1, api2: Config2}
  const result = yield* all({
    api1: call(Api.fetchConfig1),
    api2: call(Api.fetchConfig2),
  });
  ...
}

Babel Macro

You can use the built-in babel macro that will take care of transforming all your effects to raw redux-saga effects.

Install the babel macros plugin:

yarn add --dev babel-plugin-macros

Modify your import names to use the macro:

import {call, race} from "typed-redux-saga/macro";

// And use the library normally
function* myEffect() {
  yield* call(() => "foo");
}

The previous code will be transpiled at compile time to raw redux-saga effects:

import {call, race} from "redux-saga/effects";

function* myEffect() {
  yield call(() => 'foo');
}

This gives you all the benefits of strong types during development without the overhead induced by all the calls to typed-redux-saga's proxies.

ESLint Rules

In order to avoid accidentally importing the original effects instead of the typed effects, you can use this ESLint plugin: https://github.com/jambit/eslint-plugin-typed-redux-saga

It includes an auto-fix option, so you can use it to easily convert your codebase from redux-saga to typed-redux-saga!

Credits

Thanks to all the contributors and especially thanks to @gilbsgilbs for his huge contribution.

See Also

typed-redux-saga's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

typed-redux-saga's Issues

Howto wrap takeLatest/takeEvery with solution

Hey,

I have the need to enable/disable a preloader on all saga calls, after a while I found the following solution for takeEvery/takeLatest in Typescript.

import {
  put,
  takeLatest as upstreamTakeLatest,
  SagaGenerator,
  takeEvery as upstreamTakeEvery,
} from "typed-redux-saga";
import { ActionPattern, ForkEffect } from "redux-saga/effects";
import { ActionMatchingPattern } from "@redux-saga/types";
import { ConfigModule } from "../..";

export const workerWrapper = <P extends ActionPattern>(
  type: P,
  worker: (action: ActionMatchingPattern<P>) => any
): ((action: ActionMatchingPattern<P>) => any) =>
  function* wrapped(action: ActionMatchingPattern<P>): any {
    yield* put(ConfigModule.actions.setPreloader(true));
    try {
      // Call the worker / wrapped action
      yield* worker(action);
    } finally {
      yield* put(ConfigModule.actions.setPreloader(false));
    }
  };

export function takeLatest<P extends ActionPattern>(
  pattern: P,
  worker: (action: ActionMatchingPattern<P>) => any
): SagaGenerator<never, ForkEffect<never>> {
  return upstreamTakeLatest(pattern, workerWrapper(pattern, worker));
}

export function takeEvery<P extends ActionPattern>(
  pattern: P,
  worker: (action: ActionMatchingPattern<P>) => any
): SagaGenerator<never, ForkEffect<never>> {
  return upstreamTakeEvery(pattern, workerWrapper(pattern, worker));
}

I'm posting here with the hope it helps someone else.

Kind regards,
Renรฉ

yield* call sometimes infers type as Generator

const { d1, d2  } = yield* all({
    d1: call(fetchD1, options),
    d2: call(fetchD2, options),
  });

I've had sitatuions where the above example fetchD1 and fetchD2 are both other sagas that I am calling with call but fetchD1 infers the correct type ({ from: string; to: string; featureName: string | undefined; name: string; data: MeasurementCharacteristicPresentation[]; }) and fetchD2 infers,

Generator<SelectEffect | AllEffect<SagaGenerator<{
    from: string;
    to: string;
    featureName: string | undefined;
    name: string;
    data: MeasurementCharacteristicPresentation[];
}, CallEffect<...>>>

The difference between the two sagas return,

fetchD1

return yield* call(
    (from, to, take, machineId) =>
      client.fetchD1Data.unique({
        query: {
          from,
          to,
          take,
          machineId
        }
      }).promise,
    from,
    to,
    constants.maxTake,
    machineId
  );

fetchD2

return yield* all(
    items.map(({  name }) =>
      call(fetchD2Data, { from, to,  name })
    )
  );

Error handling using try / catch

Great product, finally making redux saga typesafe ๐Ÿ˜€

It appears to me that try / catch error handling does not work. Is that right? I have this scenario:

// saga function:
try {
  const result = yield* callBackend();
  yield* put(success());
} catch {
  yield* put(error());
}

// callBackend:
return yield* call(function_that_uses_fetch)

The function that uses fetch throws an error, but it is not caught in the catch clause.
Is it possible to make this work?

docstrings in type definitions

the type definitions in redux-saga itself contains a lot of nice documentation (for example) which vscode and other editors show on hover -- it would be nice to copy it over into these type definitions.

If i remember when i have time, i'll submit a pr, probably with a script to copy it over too.

Returned type of `join` effect

Hello!
I've run into an issue with returned type of join effect. In typed-redux-saga in all cases effect join returns void . In my situation I am using spawn effect in this way -
const test = yield* spawn(someFunction);
test receives type witch is returned by someFunction. Then I am using join effect like this -
const testJoin = yield* join(test);
and do expect that variable testJoin will receive type of test but it comes as void.
Is it an error or it is correct?
Is it possible that it's fixed in the way that join returns type of task given to it?

Macro doesn't seem to do anything

import { select } from 'typed-redux-saga/macro';

const getToken = (state: RootState) => state.user.account?.token; // type: (...) => string | undefined

function* fetchAlertsSaga() {
  const token = yield select(getToken); 
  // TypeScript error here 
  // TS7057: 'yield' expression implicitly results in an 'any' type because 
  //   its containing generator lacks a return-type annotation.
  // ...
}

babel-plugin-macros is installed via react-scripts dependencies:

@...
โ”œโ”€โ”ฌ [email protected]
โ”‚ โ””โ”€โ”ฌ [email protected]
โ”‚   โ””โ”€โ”€ [email protected] deduped
โ””โ”€โ”ฌ [email protected]
  โ””โ”€โ”€ [email protected]

What I do wrong? Should I configure macro somehow?

If I switch import of typed-redux-saga/macro to the typed-redux-saga and add '*' to the yields, the correct type is inferred:

  const token = yield* select(getToken);  // string | undefined

call effect does not infer the type

Tested with
TS: 3.7.2
typed-redux-saga: 1.0.6

How to reproduce?

import { select, call } from 'typed-redux-saga';


function someApiCall(): number {
	return 1;
}
function someSelect(): number {
	return 1;
}

export function* doStuff() {
	const result = yield* call(someApiCall)
	
	// variable `result` is still any
	result.lol();


	const selectResult = yield* select(someSelect)

	// gives error as expected
	selectResult.lol();
}

As you can see from the example code the the select effect infers correct type but call does not. Where is the problem? ๐Ÿค”

Yielded value from 'call' have type 'unknown'

Hi everyone! I have just bumped into the situation:

import { call } from "typed-redux-saga";

type Product = {
    id: number;
    name: string;
}

type ProductExtended = Product & {
    weight: number;
}

export function normalizeProduct<T extends Partial<Product>>(product: T): T {
    const { name = '' } = product;

    return {
        ...product,
        name: name.trim(),
    };
}

function* saga(productExtended: ProductExtended) {
    const productExtendedNormalized = yield* call(normalizeProduct, productExtended);
    // productExtendedNormalized is now of type 'unknown', but expected type is 'ProductExtended'
}

Argument of type 'string' is not assignable to parameter of type 'TakeableChannel<unknown>'.

dispatching action with payload, produces this typescript error: Argument of type 'string' is not assignable to parameter of type 'TakeableChannel'.
.

export default function* watchAuth() {
yield* takeLatest(startAuth.toString(), handleAuthUser); // Argument of type 'string' is not assignable to parameter of type 'TakeableChannel'.
}

using:
"@reduxjs/toolkit": "^1.8.5",
"typed-redux-saga": "^1.5.0",

Pass yield* effects to race and all yield* effects

From #10 (comment) :

I feel like it's unconvenient for the race and all yield* effects to take raw redux-saga's effects. Is it possible and/or desirable to pass yield* effects to them? If so, it would be quite easy to add a simple linter rule that bans redux-saga/import. I would kind of like that. WDYT?

How to create saga middleware using typed-redux-saga

I've updated to typed-redux-saga recently. Prior to this I was using redux-saga.

Saga middleware create with redux-saga looks as follows

import { applyMiddleware, compose, createStore } from "redux";

import createSagaMiddleware from "redux-saga";
import rootReducer from "../reducer";
import rootSaga from "../sagas";

declare global {
  interface window {
    __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
  }
}
const reduxDevTools =
  (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  reduxDevTools(applyMiddleware(sagaMiddleware))
);

sagaMiddleware.run(rootSaga);

export default store;

My question is do I still need to use above way of creating saga middleware from redux-saga or it should be from typed-redux-saga?

Replace `any` with `unknown`

In the non-generic forms of take & select we set the generator return type to any. I'm inclined to replace this with unknown. This would be a breaking change but I think we can get away with it while this lib is still crystalizing.

What do you think @gilbsgilbs ?

eslint-plugin-import doesn't honor tsconfig paths

Cloning #248 to a new issue because #248 got repurposed. :P

Note these paths from tsconfig.json:

    "baseUrl": ".",
    "paths": {
      "typed-redux-saga": ["."],
      "typed-redux-saga/macro": ["./macro"]
    }

It seems like eslint-plugin-import doesn't understand these, so for example tsc is happy with this import, but eslint-plugin-import believes that it is unresolved:

// eslint-disable-next-line import/no-unresolved
import * as Effects from "typed-redux-saga/macro";

We should confirm we're not doing anything wrong and--assuming we aren't--create an issue upstream.

We might need this: https://github.com/benmosher/eslint-plugin-import#typescript

Allow resolving raw promises

redux-saga allows just yielding a promise without a call effect, like

function* mySaga() {
  yield Promise.resolve()
}

although this example could be wrapped in a call, there are some obscure use cases for this.

I'd suggest, defining a resolve function/macro, which the macro transform just deletes. The typing would be like

export function resolve<P>(
  promise: P,
): SagaGenerator<P extends Promise<infer RT> ? RT : P, never>

if this would be a welcome change and i remember and/or the need becomes more pressing, i'll submit a PR. (yes i know i've said that before ๐Ÿ˜› )

Typing yield* call of a generic function

Thank you for this great library!

Does anyone know how to type the following saga call

// React View component equivalent

    apolloClient.query<GetAppVersionQuery>({query: getAppVersion, variables: { platform: Platform.IOS }}).then(res => {
      console.log(res)  // res is type GetAppVersionQuery
    })

In typed-redux-saga

    const { query } = apolloClient
    const response = yield* call(query, { query: getAppVersion, variables: { platform: Platform.IOS }})
    // response is type any, how to set return type to GetAppVersionQuery?

The original query has the type of
const query: <T=any, TVariables=Record<string, any>>(options: QueryOptions<TVariables, T>) => Promise<ApolloQueryResult<T>>

Thank you

Does this package require --downlevelIteration ?

import { call } from "typed-redux-saga/macro";

const getString = () => 'a string'

function* exampleSaga() {
  const someString = yield* call(getString)
}

In vscode, I get an error that --downlevelIteration needs to be enabled. Is this a required option for this package? Or is something wrong in my setup?

Thanks for this code, it's a real help to get better types in saga. I see they're looking to merge it into the saga repo also. Neat.

How do you type all?

do you have an idea on how to get safely typed generators that are under the all effect?

Typing for `all` forces homogenous values

all assumes a shared type between arguments, causing the type inference to devolve to any for types that don't share a more refined base type.

Promise.all handles this well, if via a bit of a hack:
all<T1, T2>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1, T2]>;

I believe typed-redux-saga could do something similar -

export type All<
    T1 extends (...args: any[]) => any,
    T2 extends (...args: any[]) => any
> =
    (toRun: [SagaGenerator<SagaReturnType<T1>>, SagaGenerator<SagaReturnType<T2>>])
    => SagaGenerator<[SagaReturnType<T1>, SagaReturnType<T2>]>

essentially it'd be nice if all just converted from [generator<T>, generator<T1>,...etc] into generator<[T, T1]>

I'd like to submit a PR for this, however I'm having trouble understanding the original signature for all - specifically the second argument for the returned SagaGenerator<EffectReturnType<T>[], AllEffect<T>>. What is that second parameter for? I imagine it will need to change (perhaps in redux-saga) to support this.

`take(channel, pattern)` support

Hey, I'm working on a PR, but I wanted to run this by you first; the Take effect also has a variant that works with channels. It's fairly trivial to add this to typed-redux-saga's implementation, but since typescript doesn't support generator overloading I had to change it to a function with an IIGE (immediately invoked generator expression :).

export function take<A extends Action>(
  channel: Channel<A>,
  pattern: ActionPattern<A>,
): SagaIterator<A>;
export function take<A extends Action>(
  pattern?: ActionPattern<A>,
): SagaIterator<A>;
export function take<A extends Action>(a?: any, b?: any): SagaIterator<A> {
  return (function*() {
    if (a.take) {
      return yield rawTake(a, b);
    } else {
      return yield rawTake(b);
    }
  })();
}

This changes the type of the export, but not the signature. It's also possible to keep the generator and avoid the overload, but it's really ugly. Are you cool with using a function for the top level export, and if so, should the rest of the exports use the same pattern? In practice, it doesn't change the developer ergonomics re: intellisense.

Don't work watchers after yield takeEvery

Hello!

I have watchers function

import {takeEvery,takeLatest} from 'typed-redux-saga'

export function* watchSmsTemplatesSagas() {
  yield takeEvery(smsTemplatesActions.startInit.type, handlerInit);
  yield takeLatest(smsTemplatesActions.delete.type, handlerDeleteSmsTemplate);
  yield takeLatest(smsTemplatesActions.submitForm.type, handlerFormSubmit);
  yield takeLatest(smsTemplatesActions.changeValidSymbols.type, handlerChangeValidSymbols);
}

All the effects which was written after the takeEvery don't work.

But!

  1. If I use only takeEvery from @redux-saga/core/effects all work good
import {takeLatest} from 'typed-redux-saga';
import {takeEvery} from '@redux-saga/core/effects';

export function* watchSmsTemplatesSagas() {
  yield takeEvery(smsTemplatesActions.startInit.type, handlerInit);
  yield takeLatest(smsTemplatesActions.delete.type, handlerDeleteSmsTemplate);
  yield takeLatest(smsTemplatesActions.submitForm.type, handlerFormSubmit);
  yield takeLatest(smsTemplatesActions.changeValidSymbols.type, handlerChangeValidSymbols);
}
  1. if I use all effects from typed-redux-saga with * all work good too
import {takeLatest, takeEvery} from 'typed-redux-saga';

export function* watchSmsTemplatesSagas() {
  yield* takeEvery(smsTemplatesActions.startInit.type, handlerInit);
  yield* takeLatest(smsTemplatesActions.delete.type, handlerDeleteSmsTemplate);
  yield* takeLatest(smsTemplatesActions.submitForm.type, handlerFormSubmit);
  yield* takeLatest(smsTemplatesActions.changeValidSymbols.type, handlerChangeValidSymbols);
}

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Rate-Limited

These updates are currently rate-limited. Click on a checkbox below to force their creation now.

  • Update dependency eslint to v9
  • Update dependency eslint-plugin-functional to v6
  • Update dependency eslint-plugin-prettier to v5
  • Update dependency prettier to v3
  • Update dependency rimraf to v5
  • Update dependency rollup to v4
  • Update dependency typescript to v5
  • Update typescript-eslint monorepo to v7 (major) (@typescript-eslint/eslint-plugin, @typescript-eslint/parser)
  • ๐Ÿ” Create all rate-limited PRs at once ๐Ÿ”

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

npm
macro/package.json
package.json
  • @babel/cli ^7.14.8
  • @babel/core ^7.15.0
  • @babel/preset-env ^7.15.0
  • @types/babel-plugin-macros ^2.8.5
  • @types/fs-extra ^9.0.12
  • @types/node ^16.7.10
  • @types/nunjucks ^3.1.5
  • @typescript-eslint/eslint-plugin ^5.0.0
  • @typescript-eslint/parser ^5.0.0
  • babel-plugin-tester ^10.1.0
  • codecov ^3.8.3
  • copyfiles ^2.4.1
  • dtslint ^4.1.6
  • eslint ^8.0.0
  • eslint-config-prettier ^8.3.0
  • eslint-config-typed-fp ^3.0.0
  • eslint-import-resolver-typescript ^2.4.0
  • eslint-plugin-functional ^4.0.0
  • eslint-plugin-import ^2.24.2
  • eslint-plugin-prettier ^4.0.0
  • eslint-plugin-total-functions ^6.0.0
  • jest ^28.0.0
  • nunjucks ^3.2.3
  • prettier ^2.3.2
  • redux-saga ^1.1.3
  • redux-saga-test-plan ^4.0.3
  • rimraf ^3.0.2
  • rollup ^2.56.3
  • ts-node ^10.2.1
  • type-coverage ^2.18.2
  • typescript ^4.4.2
  • @babel/helper-module-imports ^7.14.5
  • babel-plugin-macros ^3.1.0
  • redux-saga ^1.1.3
  • node >=10.0.0
nvm
.nvmrc

  • Check this box to trigger a request for Renovate to run again on this repository

Typing not enforced if `select` called without argument

in redux the effect select can be called without an argument to get the full state
https://redux-saga.js.org/docs/api/#selectselector-args

If you call it without an argument in typed-redux-saga though it comes back as an any type

say this was the state

{
   user: {
      name: 'foo'
   }
}
const {user} = yield* select();

user will come back with an any type so something like user.email won't cause a TS error

I think that this may be hard to implement but there should at least be an note in the README warning about it

esbuild/esbuild-loader

Anyone using esbuild/esbuild-loader know a workaround for the macro issue? I guess you could get it working with babel-loader for the sagas and esbuild-loader for the rest.

Make PR about this to redux-saga?

Since this library is very simple wrapper could you make PR about these changes to redux-saga it self so everyone would have these wonderful benefits? :)

Suggestion: Add action sensitive type effects

Hello, and thanks for this library that solved a long issue of mine with redux-saga. I
would like to suggest improved typing for effects that yield actions, i.e. take.

const action = yield* take("ADD_FOO");
// inferred type of action is Action<any>

With an expression like above, the inferred type for action is very imprecise. If
we want to safely use meta or payload of the action in the saga, we must still assert its type. Some
libraries like redux-toolkit and typesafe-actions suggest precisely typing action
creators and using them to power generic type inference in e.g. reducers. This could also
be done with redux-sagas, using the workaround of this package. I'm suggesting adding a
typesafe take effect that take an action creator instead of the action type, as below:

export function typedTake<AC extends (...args: any[]) => any>(
  pattern?: AC
): SagaGenerator<ReturnType<AC>, never>;
export function* typedTake(args: any) {
  return yield rawTake(args.type);  // uses redux-toolkit actions
}

const addTodo = createAction<string>("ADD_TODO");
// from redux-toolkit; addTodo('some text').payload has type string
// and the type is attached as addTodo.type
function* todoSaga() {
  const action = yield* typedTake(addTodo);
  // type correctly inferred
}

The above is not specific to any kind of action creator except that type attribute of
action is passed to rawTake. Overloading take from this library is not possible because
redux-saga already interprets a function argument as a filter. The above can be extended
to arrays of action creators to mirror redux-saga.

I am happy to hear any thoughts.

Improve take type

Consider when the take effect is passed a string and an action type param, like this:

  type FooAction = {type: "FOO"};
  yield* Effects.take<FooAction>("FOO"); // $ExpectType FooAction

In cases like this we should be able to constrain the passed string to "FOO" | "*", giving some extra type safety.

See https://redux-saga.js.org/docs/api/#takepattern

Regression in latest release? (when upgrading from 1.4 to 1.5)

Hi,

I added a PR with two examples of cases that started failing for us when trying to apply the update: #675

The smallest case looks like this:

  function handleArgumentThatIsPartOfUnion(requestId: 27 | 28) {
    return 22;
  }
  yield* Effects.call(handleArgumentThatIsPartOfUnion, 27);

This code works in 1.4 but in 1.5 the argument to the function called (27) gets interpreted as number (instead of that specific number it seems?)

Has anyone else had this problem? ideas to fix it?

eslint not playing nice with tsconfig paths

Note these paths from tsconfig.json:

    "baseUrl": ".",
    "paths": {
      "typed-redux-saga": ["."],
      "typed-redux-saga/macro": ["./macro"],
      "typed-redux-saga/effects-for-action-type": ["./effects-for-action-type"]
    }

It seems like eslint doesn't understand these, so for example tsc is happy with this, but eslint isn't:

import * as Effects from "typed-redux-saga/macro";

TakeLatest only registering first saga in list of sagas

Relative code

Please consider the following code snippet as you read through the issue below

import { call, put, takeLatest } from 'typed-redux-saga';

function* initialLoad() {
  ...
}

function* logout() {
  ....
}

export function* authSaga() {
  yield takeLatest(actions.logout.type, logout);
  yield takeLatest(actions.initialLoadStart.type, initialLoad);
}

Issue

When using the typed-redux-saga version of takeLatest, on the first seems to be registering properly; when calling initialLoadStart, the action is dispatched, but initialLoad is never called.

Expected result

When using the same code, but using takeLatest from redux-saga/effects both sagas get registered and are invoked when the event is dispatched

Question: Is putResolve typed correctly?

Firstly, massive thanks for this library. As I start my journey into the saga wildnerness, being able to trust the types I get back is a HUGE win. Really appreciate this library.

I'm also using redux-saga-promise-actions so that I can await sagas from within other sagas. In my adventures there, I've started to suspect that the type definitions for put and putResolve should be different, but they appear to be the same.

export function put<A extends Action>(
action: A,
): SagaGenerator<A, PutEffect<A>>;
export function put<T>(
channel: PuttableChannel<T>,
action: T | END,
): SagaGenerator<T, ChannelPutEffect<T>>;
export function putResolve<A extends Action>(
action: A,
): SagaGenerator<A, PutEffect<A>>;

In VSCode when I try const result = yield* putResolve(action()) I get the type of result as the return type of action. The same for put() instead of putResolve(). But putResolve() should be the return type of store.dispatch(action()) I think.

I've tried playing around with the types to see if I can figure out the fix, but unfortunately I'm stumped, my TypeScript fu is insufficient to the task.

Typed-redux-saga is not making API call

Redux Action:

export const getUsers = () => {
  return {
    type: GET_USERS_REQUEST,
  };
};

API Request:

export const getUsers = () => {
  return API(GET_USERS, {
    method: "GET",
  });
};

Saga:

function* callGetUsers() {
  try {
    const result = yield* getUsers();
    const { status, data } = result;
    if (status === 200) {
      yield* put({ type: GET_USERS_SUCCESS, payload: data });
    }
  } catch (err) {
    yield* put({
      type: GET_USERS_FAILED,
      payload: err.response.data,
    });
  }
}

export function* watchGetUsers() {
  yield* takeLatest(GET_USERS_REQUEST, callGetUsers);
}

Saga:

yield* fork(watchGetUsers);

After upgrading to typed-redux-saga its not making API call. Am I missing anything here?

Question: How to properly test typed-redux-saga

Hi,
First of all, thank you for this great library, it helped me improve the typing and quality of my codebase a lot !
But still do have some question about how to properly test saga that uses typed-redux-saga instead of regular redux-saga.

Let's consider this saga

import * as untypedReduxSaga from "redux-saga/effects";

export async function randomPromise(): Promise<number>{
    return 42;
}

export function* untypedTodosSaga(){
    const {p1} = yield untypedReduxSaga.all({
        p1: untypedReduxSaga.call(randomPromise),
    })
    return p1;
}

We can test it pretty easily using this test ( And the test passes โœ… )

import * as untypedReduxSaga from "redux-saga/effects";
import {randomPromise, untypedTodosSaga} from "./todosSaga";

test('regular redux saga', ()=>{
    const gen = untypedTodosSaga();
    expect(gen.next().value).toEqual(untypedReduxSaga.all({
            p1: untypedReduxSaga.call(randomPromise),
        })
    );
    expect(gen.next({p1: 44}).value).toEqual(44);
})

But if the original codebase we replace regular redux-saga) (untypedReduxSaga) by the typed version the test fail ๐ŸŸฅ

import * as typedReduxSaga from "typed-redux-saga";

export async function randomPromise(): Promise<number>{
    return 42;
}

export function* typedTodosSaga(){
    const {p1} = yield* typedReduxSaga.all({
        p1: typedReduxSaga.call(randomPromise),
    })
    return p1;
}
- Expected  - 10
+ Received  +  1

  Object {
    "@@redux-saga/IO": true,
    "combinator": true,
    "payload": Object {
-     "p1": Object {
-       "@@redux-saga/IO": true,
-       "combinator": false,
-       "payload": Object {
-         "args": Array [],
-         "context": null,
-         "fn": [Function randomPromise],
-       },
-       "type": "CALL",
-     },
+     "p1": Object {},
    },
    "type": "ALL",
  }

And the reason behind is that the yield* actually "unwrap" the all combinator, and therefore returns the actual JSON payload of the effect, but the nested call function itself remains a generator around the "real" redux-saga call.

Meaning that we can actually test that the all effect has been yield but we can't check the content of this all effect, greatly degrading the interest of our test.

We can't update the code to unwrap the call effect by hand without losing types inference.

export function* typedTodosSaga(){
    const {p1} = yield* typedReduxSaga.all({
        p1: typedReduxSaga.call(randomPromise).next().value, // ๐ŸŸฅ  p1 => unknown
    })
    return p1;
}

Workaround

The workaround I use for now is to manually "unwrap" the effect if it is a combinatorEffect (cf: all | race) using the following function.

import type {CombinatorEffect, CombinatorEffectDescriptor, Effect} from '@redux-saga/types';
import {SagaGenerator} from "typed-redux-saga";

import {map} from 'ramda';

function isCombinatorEffect<T = any, P = any>(
    effect: Effect<T, P | CombinatorEffectDescriptor<P>> | unknown,
): effect is CombinatorEffect<T, P> {
    return (effect as Effect<T, P | CombinatorEffectDescriptor<P>>).combinator;
}

export function unwrapCombinators<E extends Effect<T, P> | unknown, T, P extends SagaGenerator<any> | any>(step: E): E {
    const mapper = (value: P) => {
        if (typeof (value as SagaGenerator<any>).next === 'function') {
            return (value as SagaGenerator<any>).next().value;
        } else {
            return value;
        }
    };
    if (isCombinatorEffect(step)) {
        // @ts-ignore honestly this is way to complex to type this :/
        const newPayload = map(mapper, step.payload);

        return {...step, payload: newPayload};
    } else {
        return step;
    }
}

My test now looks like this, and passes โœ… , and the saga is still correctly typed.

test('typed redux saga (using unwrapCombinator)', ()=>{
    const gen = typedTodosSaga();
    expect(unwrapCombinators(gen.next().value)).toEqual(untypedReduxSaga.all({
            p1: untypedReduxSaga.call(randomPromise),
        })
    );
    expect(gen.next({p1: 44}).value).toEqual(44);
})

Suggestions

  • Maybe the combinators effect can always recursively unwrap nested effects
  • Or, this library can provide a helper function like unwrapCombinators to help deal with it
  • Or provide/document a better alternative

Type inference not working with call

image

For some reason TypeScript always infers the result of call as "any". Yes, I am importing the call function from typed-redux-saga, not regular redux-saga.

Here is an example usage:

export function* attemptLoginSaga(action: ReturnType<typeof attemptLogin>) { try { const tti = { login() { return new Promise<number>((resolutionFunc, rejectionFunc) => { resolutionFunc(777) }) } } const response = yield* call(tti.login) }

Here is my tsconfig file. I am on create-react-app and typescript 4.2.3:

{ "include": ["src"], "compilerOptions": { "baseUrl": "src", "downlevelIteration": true, "target": "ES6", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": false, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "noImplicitAny": false, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "noFallthroughCasesInSwitch": true } }

Context "this" is not correctly resolved.

In use with some external libraries this seems to be not resolved correctly and causes error.
image
Above case resulted using https://github.com/ethers-io/ethers.js/ library.

  const signer = yield* call(getSigner)
  const balance = yield* call(signer.getBalance)

getBalance should return BigNumber but instead fails.

Refactoring function into Promise solves this problem.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.