what could be the payload data type?

Viewed 1525

I am running a graphql mutation:

interface SignInReponse {
  loginEmail : {
    accessToken: number;
  }
}

 const [login] = useMutation<SignInReponse>(LOGIN);

let submitForm = (email: string, password: string) => {
    setIsSubmitted(true);
    login({
      variables: {
        email: email,
        password: password,
      },
    })
      .then(({ data }: ExecutionResult<SignInReponse>) => {
        if (data !== null && data !== undefined){
        dispatch({ type: "login", payload: data.loginEmail.accessToken })  
        setTimeout(_ => {
          setShouldRedirect(true);
        }, 2000)
        setIsLoggedIn(true);
  }})

My login reducer:

const initialState = {
    token: null
};

export const loginReducer = (state = initialState, { type, payload } :any) => {
  switch (type) {
    case "login":
      return {...state, token: payload};
    default:
      return state;
  }
};

I want to change any data type in { type, payload } :any and be more specific.

However, I don't understand what's the correct way to find/use the right data type. I have already tried using SignInResponse but that doesn't work since type/payload aren't mentioned in it.

EDIT:

I also tried this:

export const loginReducer = (state = initialState, { type, payload }: {type: "login", payload: string })

but it doesn't seem to work.

TypeScript error in /helpers/privateRouting.tsx(7,32):
Property 'token' does not exist on type 'never'.  TS2339

I modified things like this but it still gives an error:

actions/login.ts:

const LOGIN = 'LOGIN'

export interface LoginAction {
    type: typeof LOGIN
    payload: string;
  }

allActions.ts:

import { LoginAction } from './login';

export type AnyAction = LoginAction

Login Reducer:

const initialState = {
    token: null
};
export const loginReducer = (state = initialState, action: AnyAction) => {
  switch (action.type) {
    case 'LOGIN':
      return {...state, token: action.payload};
    default:
      return state;
  }
};

Reducer index.ts:

export const rootReducer = combineReducers({
    login: loginReducer,
});

PrivateRouting:

export const PrivateRoute = ({ component, ...rest }: any) => {
  const routeComponent = (props: any) => {
    if (store.getState().login.token) {
      return React.createElement(component, props);
    } else {
      return <Redirect to={{ pathname: '/404' }} />;
    }
  };
  return <Route {...rest} render={routeComponent} />;
};

Error:

Property 'token' does not exist on type 'never'.  TS2339

     5 | export const PrivateRoute = ({ component, ...rest }: any) => {
     6 |   const routeComponent = (props: any) => {
  >  7 |     if (store.getState().login.token) {

Store index.ts:

const persistConfig = {
    key: 'root',
    storage
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

let store = createStore(persistedReducer);
let persistor = persistStore(store);
export { store, persistor };

I am using the privateRoutes like this:

export default function App() {
  return (
    <Provider store={store}>
      <PersistGate persistor={persistor}>
        <div>
          <BrowserRouter>
            <Switch>
              <Route path="/login" component={LoginPage}></Route>
              <Route path="/404" component={Error404Page}></Route>
              <PrivateRoute
                path="/add"
                component={AddUserPage}
              />
              <Redirect from="*" to="/404" />
            </Switch>
          </BrowserRouter>
        </div>
      </PersistGate>
    </Provider>
  );
}
2 Answers

This is a common theme when using Redux with TypeScript. You should use a union type exported from some big main actions.ts file in an important place. For example:

// somewhere/ComponentOne.tsx
export default ComponentOne = () => stuff;

export interface OneAction {
  type: 'ONE_ACTION';
  payload: string;
}
// all-actions.ts
import { OneAction } from './somewhere/ComponentOne';
import { AnotherAction } from './somewhere/AnotherComponent';

export default type AnyAction = OneAction | AnotherAction;

Then in your reducer:

import { AnyAction } from '../../all-actions.ts';

function loginReducer(state = initialState, action: AnyAction) {
  // stuff
}

Now your reducer knows exactly what it's getting and the type safety is perfect. This article explains it much better than I can and is highly worth a read.

Solution: Add an interface for initialState, and use it as the generic type parameter for combineReducers.

interface StoreState {
  token: string | null
}
const initialState: StoreState = ...;
export const rootReducer = combineReducers<StoreState>({...})

Reasoning: The error occurs at store.getState(), and redux has type declaration for getState() like

/**
 * @template S State object type.
 */
export interface Store<S> {
  //...
  getState(): S;
  //...
}

export const createStore: StoreCreator;
export interface StoreCreator {
  <S>(reducer: Reducer<S>, enhancer?: StoreEnhancer<S>): Store<S>;
  <S>(reducer: Reducer<S>, preloadedState: S, enhancer?: StoreEnhancer<S>): Store<S>;
}

(you can find the above in node_modules/redux/index.d.ts.)

So it looks like our code didn't specify the generic type parameter S for Store<S>. Looking at the code above, we see that typescript should know S in Store<S> if we specify it in createStore call, or better, infer S from the function param reducer: Reducer<S>.

So the goal now is to specify S for the(any one) param in createStore(persistReducer(persistConfig, rootReducer)). I'm missing the type of persistReducer here, but I suppose it gets S from rootReducer.

So the goal becomes specify S for combineReducers({login: loginReducer});, while combineReducers has a signature of:

export function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>;

Oh! The result of combineReducers(Reducer<S>) can only get S from explicit specified type param in combineReducers<MyType>(...) (no refering from function param like some functions mentioned above). So we specify it.

Related