zalmoxisus / redux-devtools-extension

Redux DevTools extension.
MIT License
13.5k stars 1.01k forks source link

actionSanitizer & stateSanitizer with TypeScript #733

Open fvilers opened 4 years ago

fvilers commented 4 years ago

Hi !

I'd like to use these EnhancerOptions with TypeScript and my typed state but can't find a way to set them correctly. I don't want to use any and to lose types.

Here's a small reproductible example (type StateSanitizer comes from redux-devtools) :

type AppState = {
    msg: string;
    secret: string;
}
type StateSanitizer = <S>(state: S) => S;

const state: AppState = { msg: "Hello", secret: "SECRET !" };
const sanitizer: StateSanitizer = (state: AppState) => {
    const result = { ...state };
    result.secret = '';
    return result; // Casting to AppState doesn't help
};

Which result in a compiler error :

Type '(state: AppState) => { msg: string; secret: string; }' is not assignable to type 'StateSanitizer'.
  Types of parameters 'state' and 'state' are incompatible.
    Type 'S' is not assignable to type 'AppState'.

I tried this too (same error) :

const sanitizer: StateSanitizer = <S extends AppState>(state: S) => {
    const result = { ...state };
    result.secret = '';
    return result as S;
};

I'm probably missing something but can't find what.

Here's my application store creation file :

import { applyMiddleware, createStore } from "redux";
import { composeWithDevTools, EnhancerOptions } from "redux-devtools-extension";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
import { AppState } from "./state";

const stateSanitizer = (state: AppState) => state;
const options: EnhancerOptions = { stateSanitizer };
const composeEnhancers = composeWithDevTools(options);

const store = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(thunk))
);

export default store;

Which fails with the same issue at compile-time.

Thanks for you help and suggestions ;-)

pkuczynski commented 3 years ago

Something like this should work:

const composeEnhancers = composeWithDevTools({
    stateSanitizer: <S extends Partial<AppState>>(state: S): S => {
        const { user, ...rest } = state

        return {
            ...rest
        } as S
    }
})
thepuzzlemaster commented 3 years ago

That can likely be closed.

isaaclyman commented 2 years ago

Is there a similar example for actionSanitizer? This is the furthest I've gotten:

actionSanitizer: <S extends Action<string>>(action: S): S => {
  if (action.type === 'LOAD_DATA') {
    action.data; // TypeScript compiler throws an error on this line
  }
  return action;
}

I've tried using <S extends Action<string> & {data: any}> but then I get an error because {data: any} doesn't fit the type constraint on the function.

isaaclyman commented 2 years ago

Update: I've got it working with the following:

actionSanitizer: <S extends Action<string>>(action: S & {payload: any}): S => {
  if (action.type === 'LOAD_DATA') {
    action.payload; // This works
  }
  return action;
}

In my case I'm using flux-standard-action so actions have a payload attached.

nathan-kansu commented 2 years ago

If your action(s) have a payload prop, you can also use the AnyAction type from redux:

const actionSanitizer = <A extends AnyAction>(action: A) => {
    ....
}
LichP commented 1 month ago

For me the magic incantation is

actionSanitizer: <A extends Action>(action: A & { payload: any} | A): A => {
  if (action.type === 'MY_ACTION_TYPE' && 'payload' in action) {
    // return modified action
  }
  return action
}

I guess the reason is that we don't know if any given passed action will have a payload property (or whatever custom property your playing with), so we have to tell it that the action can either be action with a payload or any other action.