Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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"'.    
like image 662
Tamim Avatar asked Nov 16 '25 14:11

Tamim


1 Answers

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

like image 59
captain-yossarian Avatar answered Nov 19 '25 10:11

captain-yossarian