redux-toolkit icon indicating copy to clipboard operation
redux-toolkit copied to clipboard

Strange typescript behavior with fulfillWithValue

Open jayKayEss opened this issue 2 years ago • 1 comments

I'm having a little trouble understanding the typescript payload types from an async thunk. Given the following code:

export interface UserCompaniesState {
  areUserCompaniesLoaded: boolean;
  companies?: Array<PaidolUser>;
}

export const initialState: UserCompaniesState = {
  areUserCompaniesLoaded: false,
  companies: undefined,
};

export const getUserCompanies = createAsyncThunk(
  'userCompanies/getUserCompanies',
  async (user_id: string) => {
    return (
      API.graphql(
        graphqlOperation(paidolUserByUserId, {
          user_id,
        })
      ) as Promise<GraphQLResult<PaidolUserByUserIdQuery>>
    ).then(
      (response) =>
        response.data?.paidolUserByUserId?.items.filter(isPaidolUser) || []
    );
  }
);

const userCompaniesSlice = createSlice({
  name: 'userCompanies',
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(getUserCompanies.fulfilled, (state, action) => {
      state.areUserCompaniesLoaded = true;
      state.companies = action.payload;
    });
  },
});

export function isPaidolUser(
  paidolUser: { __typename: 'PaidolUser' } | null
): paidolUser is PaidolUser {
  return paidolUser !== null;
}

I get the following typescript error when I try to assign the thunk payload to state in my reducer:

Type 'PaidolUser[] | FulfillWithMeta<never, never>' is not assignable to type 'WritableDraft<PaidolUser>[] | undefined'.
  Type 'FulfillWithMeta<never, never>' is missing the following properties from type 'WritableDraft<PaidolUser>[]': length, pop, push, concat, and 29 more.

I'm not sure what the FulfillWithMeta<never, never> type is used for and I'm having a hard time googling for answers.

BUT! If I simply include { fulfillWithValue } in my thunk args, even though I'm not calling that function everything works out OK:

export const getUserCompanies = createAsyncThunk(
  'userCompanies/getUserCompanies',
  async (user_id: string, { fulfillWithValue }) => {
    return (
      API.graphql(
        graphqlOperation(paidolUserByUserId, {
          user_id,
        })
      ) as Promise<GraphQLResult<PaidolUserByUserIdQuery>>
    ).then(
      (response) =>
        response.data?.paidolUserByUserId?.items.filter(isPaidolUser) || []
    );
  }
);

In this case, action.payload in my reducer is correctly typed as:

(parameter) action: PayloadAction<PaidolUser[], string, {
    arg: string;
    requestId: string;
    requestStatus: "fulfilled";
}, never>

And I'm able to make the assignment state.companies = payload.action without any extra type guarding.

Strangely, if I use fulfillWithValue() to return my value from the thunk, then it's typed as unknown in the action payload.

What is the FulfillWithMeta<never, never> type used for, and should I be explicitly guarding against it in my reducer?

jayKayEss avatar Jun 17 '22 14:06 jayKayEss

It works for me in this TypeScript Playground

Could you try to create a TypeScript playground where it is not working for you? Have you checked that your tsconfig.json has strict: true?

phryneas avatar Jun 17 '22 16:06 phryneas