Typescript: Issue encountered with Record type causing Type Error

When utilizing handler functions within an object, the Record type is used in the following code snippet:

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
         };
     }
 };

An error occurs when trying to use the handler functions:

TS2322: Type '(state: State, action: InitializeAction) => State' is not assignable to type '(state: State, action: Action) => State'.   
  Types of parameters 'action' and 'action' conflict.    
    Type 'Action' does not match 'InitializeAction'.      
      Type 'LoginAction' cannot be assigned to 'InitializeAction'.     
        Conflicting types in property 'type'.         
          'Type "LOGIN"' is not the same as 'Type "INITIALIZE"'.          

Answer №1

One possible reason for this behavior could be related to contravariance. For more insights on this topic and how it applies in TypeScript, check out this resource.

To properly type your `handler` object, consider using 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 = {
    [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
        };
    }
};

Access the Playground here

For a related answer, you can also refer to this page.

Here's an illustrative example:


type Reducer = (state: State, action: Action) => State;
const reducer: Reducer = (state, action) => state

type ContravariantReducer = Record<string, Reducer>;

// Inheritance arrow has switched directions
const contravariant: ContravariantReducer = {
    initialize: (state: State, action: InitializeAction) => state
}

// Inheritance arrow has switched directions
const contravariant2: Record<string, (state: State, action: InitializeAction) => State> = {
    initialize: (state: State, action: Action) => state
}

P.S. Besides the type-related challenges in TypeScript, it's recommended to follow the typing guidelines for reducers as outlined in the Redux documentation by @Dima Parzhitsky

Answer №2

Although it may seem a bit messy, this solution gets the job done:

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

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

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

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Error in Angular 7: ActivatedRoute paramId returns null value

On page load, I am trying to subscribe to my paramsID, but when I use console.log(), it returns null. I am currently working with Angular 7. Here is my TypeScript code: import { Component, OnInit } from '@angular/core'; import { Activat ...

Trouble with references in Vue TypeScript when trying to access child component methods

I'm encountering an issue with calling a function in a child component while using typescript <notification ref="notification"></notification> <button @click="$refs.notification.show()"></button> Is there a ...

Repeated calls to Angular's <img [src]="getImg()"> frequently occur

Can someone help me figure out what's going on here? Recently, I set up a new Angular project and in app.component.html, I have the following code: <img [src]="getDemoImg()"> In app.component.ts: getDemoImg(){ console.log('why so m ...

Tips for resolving the issue of dropdown menus not closing when clicking outside of them

I am currently working on an angular 5 project where the homepage consists of several components. One of the components, navbarComponent, includes a dropdown list feature. I want this dropdown list to automatically close when clicked outside of it. Here i ...

"Refining MongoDB queries with a filter after performing a populate

I want to retrieve all records with populated attributes in a query. Here is the TypeScript code: router.get("/slice", async (req, res) => { if (req.query.first && req.query.rowcount) { const first: number = parseInt(req.qu ...

Generic Typescript Placeholder Design

Imagine you have the following data: const dataA = { name: "John", age: 25, attributes: {specificA: "hello", specificA2: 14, nonspecific:"Well"}, } const dataB = { name: "Lisa", age: 38, attributes: {spe ...

Stop openapi-generator from altering enum names in JavaScript/TypeScript

We have implemented the openapi generator to create our REST client and it has been quite effective. However, we encountered an issue when using enums in the format UPERCASE_UNDERSCORE, as it ended up removing the underscores which caused inconvenience. Th ...

The usage of Angular Tap is no longer recommended or supported

My Angular application contains the following HTTP interceptor: import { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpResponse } from '@angular/common/http'; ...

What is the best method for transforming an object into an interface without prior knowledge of the keys

I am looking for a solution to convert a JSON into a TypeScript object. Here is an example of the JSON data: { "key1": { "a": "b" }, "key2": { "a": "c" } } The keys key1 and key2 a ...

Tips on personalizing the formatting alert in Webclipse for Angular 2 using Typescript

Webclipse offers extensive formatting warnings for TypeScript code, such as detecting blank spaces and suggesting the use of single quotes over double quotes. However, some users find the recommendation to use single quotes annoying, as using double quotes ...

Preventing Event Propagation in Angular HTML

I am encountering an issue with stopPropagation, and I need assistance with implementing it in HTML and TypeScript for Angular. The problem is that the dialog opens but also triggers a propagation. Below is my code snippet in HTML: <label for="tab-two ...

Building an array from scratch in Angular

Below is the URL to access the code: https://stackblitz.com/edit/ng-zorro-antd-start-xz4c93 Inquiring about creating a new array. For example, upon clicking the submit button, the desired output should resemble the following structure: "tasks": [ { ...

Type-constrained generic key access for enhanced security

While attempting to create a versatile function that retrieves the value of a boolean property using a type-safe approach, I encountered an issue with the compiler not recognizing the type of my value. type KeyOfType<T, V> = keyof { [P in keyof T a ...

How to arrange an array of TypeScript strings with accents?

My array sorting function works perfectly until it encounters special characters like Á or Ű. Is there a specific TypeScript or Angular 2 method that can help me solve this issue? Here is an example of the sorting method I am currently using: private s ...

Angular: Refresh Mat-Table data following any changes

I'm currently working with a Mat-table that has expandable rows, each containing an update functionality for the data. While the POST and GET requests are functioning properly, I encounter an issue where I need to reload the page in order to see the u ...

No recommended imports provided for React Testing Library within the VS Code environment

I am currently in the process of setting up a Next JS project with Typescript integration and utilizing React Testing Library. Unfortunately, I'm facing an issue with getting the recommended imports to work properly within my VS Code environment. To i ...

Issues with compiling arise post downloading the latest Angular 2 quickstart files

My Angular 2 project was running smoothly on version 2.3, but I decided to upgrade to version 2.4. To do so, I downloaded the latest quickstart files from https://github.com/angular/quickstart After replacing my tsconfig.json, package.json, and systemjs.c ...

Utilizing span elements to display error messages

Currently, I am using a directive on a field to prevent users from entering HTML tags and JavaScript events. However, I am encountering a few challenges: a) My goal is to display an error message immediately when a user tries to input HTML tags or JavaScr ...

Issue: unable to establish a connection to server at localhost port 5000 while using Next.js getServerSideProps function

I am experiencing an issue with connecting to an API on localhost:5000. The API works perfectly when called from Postman or the browser, but it does not work when called inside Next.js getserverside props: mport { useEffect,useState } from "react"; i ...

Combining objects in JavaScript

I am currently working on converting the object received from the server into a format compatible with the backend system. I have a received object that looks like this { 'User.permissions.user.view.dashboard': true, 'Admin.permissio ...