dphilipson / typescript-fsa-reducers

Fluent syntax for defining typesafe reducers on top of typescript-fsa.
MIT License
220 stars 16 forks source link

Type mismatch in handler when using async ActionCreator #16

Closed hissfield closed 7 years ago

hissfield commented 7 years ago

Hi!

I've run into this problem when trying out your library.

I am using an async action creator that I initialize like so:

const Login = actionCreator.async<ICredentials, IUser, IError>("LOGIN");

Then my reducer looks like the following:

const reducer = reducerWithInitialState(initialState)
    .case(
        Login.done,
        (state, user) => {
            return {
                user,
            };
        },
    );

However the TS-Compiler complains about the type of user here because user is wrapped inside a Success.

    TS2345: Argument of type '(state: ISessionState, user: Success<ICredentials, IUser>) => { user: Success<ICredentials, IUser
...' is not assignable to parameter of type 'Handler<ISessionState, ISessionState, Success<ICredentials, IUser>>'.
  Type '{ user: Success<ICredentials, IUser>; }' is not assignable to type 'ISessionState'.
    Types of property 'user' are incompatible.
      Type 'Success<ICredentials, IUser>' is not assignable to type 'IUser'.
        Property 'name' is missing in type 'Success<ICredentials, IUser>'.
dphilipson commented 7 years ago

Hi Hissfield,

I admit I've never used the async action creators from typescript-fsa before, so I may be missing something, but it looks to me like this is the intended behavior. From the example here, doSomething.done produces actions that look like this:

{
    type: 'DO_SOMETHING_DONE',
    payload: {
        params: {foo: 'lol'},
        result: {bar: 42},
    }
}

where {bar: 42} is the equivalent of IUser from your example. In other words, the payload of Login.done is not just an IUser but something that looks like

{
    params: ICredentials;
    result: IUser;
}

and so the error is correct in that you should not be treating the payload like an IUser. Seems like what you want is

const reducer = reducerWithInitialState(initialState)
    .case(
        Login.done,
        (state, payload) => {
            return {
                user: payload.result
            };
        },
    );

or if you prefer the syntax of the following,

const reducer = reducerWithInitialState(initialState)
    .case(
        Login.done,
        (state, { result: user }) => {
            return {
                user
            };
        },
    );

Does that sound right? Let me know if I'm misunderstanding.

hissfield commented 7 years ago

Thanks for the explanation. It helped me track down the problem.

I am trying out redux-saga and so I am working with generators. It turns out that yield statements are not typesafe and this combined with an oversight on my part produced this issue.

I was not passing the right arguments to the Login.done action creator.

I did

const user = yield call(login, action.payload);
yield put(Login.done(user));

whereas it should have been

const user = yield call(login, action.payload);
yield put(Login.done({
    params: action.payload,
    result: user,
}));

where action.payload is the payload resulting from the previous step, Login.started.

This lead to the reducer receiving and IUser instead of a Success, but the compiler still thought it was receiving a Success.

Anyway, issue solved. Thanks again!