Rajohan
Rajohan

Reputation: 1415

TypeScript: Removing index signature without getting implicitly has an 'any' type error

Details

I am trying to create a helper function to combine my reducers to have a global reducer to use with react's useReducer hook and the context API. I have the functionality of it working but, I am currently not happy with the type checking.

Problem

To get it working I had to add [key: string]: Reducer<any> in the Reducers type and [key: string]: any; in the GlobalState type. If I remove them I get the errors

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'GlobalState'.   No index signature with a parameter of type 'string' was found on type 'GlobalState'.

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Reducers'.   No index signature with a parameter of type 'string' was found on type 'Reducers'.

This leaves me with a combineReducers function where I can name the state key anything

An example

I have a initial state like this: {page: {some state}, state2: {some state}} and then I combine the reducers like this: combineReducers({ pages: pageReducer, state3: reducer2 });

I would now have a state looking like {page: {some state}, state2: {some state}}, pages: {some state}, state3: {some state} because of "accidentally" naming the state keys wrong.

Question

Is there a way to get this to work when removing [key: string]: Reducer<any> in the Reducers type and [key: string]: any; in the GlobalState type?

Code

types.ts

export type GlobalReducer = (state: GlobalState, action: Action) => GlobalState;
export type Reducer<State> = (state: State, action: Action) => State;

export type Reducers = {
    [key: string]: Reducer<any>;
    page: Reducer<PageState>;
    state2: Reducer<AnotherState>;
};

export type GlobalState = {
    [key: string]: any;
    page: PageState;
    state2: AnotherState;
};

export type PageState = {
    title: string;
    loading: true;
};

export type AnotherState = {
    hello: string;
};

combineReducers.ts

import _ from "lodash";

import { Action, Reducers, GlobalState, GlobalReducer } from "./types";

const combineReducers = (reducers: Reducers): GlobalReducer => {
    return (state: GlobalState, action: Action): GlobalState => {
        const reducerKeys = Object.keys(reducers);

        reducerKeys.forEach((key) => {
            const newState = reducers[key](state[key], action);
            state = _.isEqual(newState, state[key]) ? state : { ...state, [key]: newState };
        });

        return state;
    };
};

export { combineReducers };

index.ts

export const globalReducer = combineReducers({ page: pageReducer, state2: reducer2 });

initialState.ts

export const initialState: GlobalState = {
    page: {
        title: "Holidaze",
        loading: true
    },
    state2: {
        hello: ""
    }
};

Upvotes: 1

Views: 563

Answers (1)

tguichaoua
tguichaoua

Reputation: 173

Object.keys return an array of string. You have to cast into a array of keyof Reducers.

const reducerKeys = Object.keys(reducers) as (keyof Reducers)[];

type GlobalState = {
    page: State1,
    anotherPage: AnotherState,
}

type GlobalReducer<State> = (state: State, action: Action) => State;
type Reducer<State> = (state: State, action: Action) => State;

type Reducers<State> = { [K in keyof State]: Reducer<State[K]> }

const combineReducers = <State>(reducers: Reducers<State>): GlobalReducer<State> => {
    return (state: State, action: Action): State => {
        const reducerKeys = Object.keys(reducers) as (keyof State)[];

        reducerKeys.forEach((key) => {
            const newState = reducers[key](state[key], action);
            state = _.isEqual(newState, state[key]) ? state : { ...state, [key]: newState };
        });

        return state;
    };
};

Upvotes: 2

Related Questions