Search code examples
typescriptreact-typescript

Typescript: Type Error while using Record type


For following code (see Playground Link) of handler functions in an object I use the Record type:

interface User {
    id: string;
    avatar: string;
    email: string;
    name: string;
    role?: string;
    [key: string]: any;
}

interface State {
    isInitialized: boolean;
    isAuthenticated: boolean;
    user: User | null;
}

type InitializeAction = {
    type: 'INITIALIZE';
    payload: {
        isAuthenticated: boolean;
        user: User | null;
    };
};

type LoginAction = {
    type: 'LOGIN';
    payload: {
        user: User;
        isAuthenticated: boolean;
    };
};

type LogoutAction = {
    type: 'LOGOUT';
};

type RegisterAction = {
    type: 'REGISTER';
    payload: {
        user: User;
    };
};

type Action =
    | InitializeAction
    | LoginAction
    | LogoutAction
    | RegisterAction;

const handlers: Record<string, (state: State, action: Action) => State> = {
    INITIALIZE: (state: State, action: InitializeAction): State => {
        const {
            isAuthenticated,
            user
        } = action.payload;

        return {
            ...state,
            isAuthenticated,
            isInitialized: true,
            user
        };
    },
    LOGIN: (state: State, action: LoginAction): State => {
        const { user } = action.payload;

        return {
            ...state,
            isAuthenticated: true,
            user
        };
    },
    LOGOUT: (state: State): State => ({
        ...state,
        isAuthenticated: false,
        user: null
    }),
    REGISTER: (state: State, action: RegisterAction): State => {
        const { user } = action.payload;

        return {
            ...state,
            isAuthenticated: true,
            user
        };
    }
};

I do get following error for the handler functions though:

TS2322: Type '(state: State, action: InitializeAction) => State' is not assignable to type '(state: State, action: Action) => State'.   
  Types of parameters 'action' and 'action' are incompatible.     
    Type 'Action' is not assignable to type 'InitializeAction'.       
      Type 'LoginAction' is not assignable to type 'InitializeAction'.         
        Types of property 'type' are incompatible.           
          Type '"LOGIN"' is not assignable to type '"INITIALIZE"'.    

Solution

  • I believe this is because of contravariance. Here you can find more information about this topic and how it works in typescript.

    In order to type your handler object, you should use mapped types:

    interface User {
        id: string;
        avatar: string;
        email: string;
        name: string;
        role?: string;
        [key: string]: any;
    }
    
    interface State {
        isInitialized: boolean;
        isAuthenticated: boolean;
        user: User | null;
    }
    
    type InitializeAction = {
        type: 'INITIALIZE';
        payload: {
            isAuthenticated: boolean;
            user: User | null;
        };
    };
    
    type LoginAction = {
        type: 'LOGIN';
        payload: {
            user: User;
            isAuthenticated: boolean;
        };
    };
    
    type LogoutAction = {
        type: 'LOGOUT';
    };
    
    type RegisterAction = {
        type: 'REGISTER';
        payload: {
            user: User;
        };
    };
    
    type Action =
        | InitializeAction
        | LoginAction
        | LogoutAction
        | RegisterAction;
    
    
    // type Handlers = {
    //     INITIALIZE: (state: State, action: InitializeAction) => State;
    //     LOGIN: (state: State, action: LoginAction) => State;
    //     LOGOUT: (state: State, action: LogoutAction) => State;
    //     REGISTER: (state: State, action: RegisterAction) => State;
    // }
    type Handlers = {
        [Type in Action['type']]: (state: State, action: Extract<Action, { type: Type }>) => State
    }
    
    
    const handlers: Handlers = {
        INITIALIZE: (state, action) => {
            const {
                isAuthenticated,
                user
            } = action.payload;
    
            return {
                ...state,
                isAuthenticated,
                isInitialized: true,
                user
            };
        },
        LOGIN: (state, action) => {
            const { user } = action.payload;
    
            return {
                ...state,
                isAuthenticated: true,
                user
            };
        },
        LOGOUT: (state) => ({
            ...state,
            isAuthenticated: false,
            user: null
        }),
        REGISTER: (state, action) => {
            const { user } = action.payload;
    
            return {
                ...state,
                isAuthenticated: true,
                user
            };
        }
    };
    

    Playground

    Here you can find related answer.

    See this example:

    
    type Reducer = (state: State, action: Action) => State;
    const reducer: Reducer = (state, action) => state
    
    type ContravariantReducer = Record<string, Reducer>
    
    // Arrow of inheritance has changed in an opposite way
    const contravariant: ContravariantReducer = {
        initialize: (state: State, action: InitializeAction) => state
    }
    
    // Arrow of inheritance has changed in an opposite way
    const contravariant2: Record<string, (state: State, action: InitializeAction) => State> = {
        initialize: (state: State, action: Action) => state
    }
    

    P.S. Apart from typescript type issues, I strongly believe that you should type your reducer according to redux docs provided by @Dima Parzhitsky