Identifying the specific type within a union of types using a discriminator

How can I specify the correct typing for the action argument in the function withoutSwitchReducer as shown below?

enum ActionTypesEnum {
    FOO = 'FOO',
    BAR = 'BAR',
}

type ActionTypes = {
    type: ActionTypesEnum.FOO,
    payload: { username: string }
} | {
    type: ActionTypesEnum.BAR,
    payload: { password: string },
};

// The existing "withSwitchReducer" successfully infers the discriminator from action.type

function withSwitchReducer(action: ActionTypes) {
    switch (action.type) {
        case 'FOO':
            return action.payload.username;
        case 'BAR':
            return action.payload.password;
        default:
            return null;
    }
}

// The issue arises below due to a lack of IntelliSense when trying to access properties that don't exist on certain payloads

const withoutSwitchReducer = {
    [ActionTypesEnum.FOO]: (action: ActionTypes) => {
        return action.payload.username;
    },
    [ActionTypesEnum.BAR]: (action: ActionTypes) => {
        return action.payload.password;
    }
};

For easier understanding, you can view the same code with IntelliSense here: TS Playground Link

Answer №1

Here are a couple of approaches to tackle this problem:

One option is to define the type once:

const withoutSwitchHandler: { [key in ActionTypesEnum]: (action: Extract<ActionTypes, { type: key }>) => string } = {
    [ActionTypesEnum.FOO]: (action) => {
        return action.payload.username;
    },
    [ActionTypesEnum.BAR]: (action) => {
        return action.payload.password;
    },
};

Alternatively, you can specify them separately:

const withoutSwitchHandler2 = {
    [ActionTypesEnum.FOO]: (action: Extract<ActionTypes, { type: ActionTypesEnum.FOO }>) => {
        return action.payload.username;
    },
    [ActionTypesEnum.BAR]: (action: Extract<ActionTypes, { type: ActionTypesEnum.BAR }>) => {
        return action.payload.password;
    },
};

The advantage of declaring the type once is that you avoid repetition, while defining them individually enables type inference for the return values of those functions.

UPDATE: A suggestion from Titian Cernicova-Dragomir in the comments is to declare it as a reusable type:

type HandlerMap<T extends { type: string }> = { [P in T['type']]: (action: Extract<T, { type: P }>) => any }

The function's return type is any. Although attempting to infer the actual return value for each definition may not be feasible.

Considering it's utilized as a reducer map, the output value might not be crucial since it's consumed by the framework.

Answer №2

ActionTypes is considered a composite type, therefore when declaring a variable of this type, it is important to explicitly define its specific type using the as keyword.

Solution:

function withSwitchReducer(action: ActionTypes) {
    switch (action.type) {
        case 'FOO':
            return (action.payload as {
                type: ActionTypesEnum.FOO,
                payload: { username: string }
            }).username;
        case 'BAR':
            return (action.payload as {
              type: ActionTypesEnum.BAR,
              payload: { password: string }
            }).password;
        default:
            return null;
    }
}

const withoutSwitchReducer = {
    [ActionTypesEnum.FOO]: (action: ActionTypes) => {
        return (action.payload as {
            type: ActionTypesEnum.FOO,
            payload: { username: string }
        }).username;
    },
    [ActionTypesEnum.BAR]: (action: ActionTypes) => {
        return (action.payload as {
          type: ActionTypesEnum.BAR,
          payload: { password: string }
        }).password;
    }
};

A more efficient solution:

interface Foo {
    type: 'FOO'
    payload: { username: string }
}

interface Bar {
    type: 'BAR'
    payload: { password: string }
}

type ActionTypes = Foo | Bar

function withSwitchReducer(action: ActionTypes) {
    switch (action.type) {
    case 'FOO':
        return (action.payload as Foo).username;
    case 'BAR':
        return (action.payload as Bar).password;
    default:
        return null;
    }
}

const withoutSwitchReducer = {
    'FOO': (action: ActionTypes) => {
        return (action.payload as Foo).username;
    },
    'BAR': (action: ActionTypes) => {
        return (action.payload as Bar).password;
    }
};

In TypeScript, you can use string literals as types, for example var a: 'Apple'. You can also combine these string literal types, like var b: 'Apple' | 'Orange'.

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

To dismiss a popup on a map, simply click on any area outside the map

Whenever I interact with a map similar to Google Maps by clicking on various points, a dynamically generated popup appears. However, I am facing an issue where I want to close this popup when clicking outside the map area. Currently, the code I have writte ...

Adding an item into a list with TypeScript is as simple as inserting it in the correct

I am working with a list and want to insert something between items. I found a way to do it using the reduce method in JavaScript: const arr = [1, 2, 3]; arr.reduce((all, cur) => [ ...(all instanceof Array ? all : [all]), 0, cur ]) During the fir ...

Experiencing a hitch when attempting to deploy an Angular 2 application on Heroku

Encountering the sh: 1: tsc: not found Error when attempting to deploy an Angular 2 app on Heroku. Using Node version: v7.2.0 and npm Version: v4.0.3. View the error on Heroku Any suggestions on how to resolve this issue? ...

Vue + TypeScript prop type issue: "'Foo' is intended as a type, but is being treated as a value in this context."

As a newcomer to TypeScript and the Vue Composition API, I encountered an error that left me puzzled: I have a component that requires an api variable as a prop, which should be of type AxiosInstance: export default defineComponent({ props: { api: A ...

Challenges encountered while implementing generic types in TypeScript and React (including context provider, union types, and intersection

I have a fully functional example available at this link: The code is working properly, but TypeScript is showing some errors. Unfortunately, I've run out of ideas on how to make the code type safe. I've searched extensively for examples that ma ...

Using TypeScript to test a Vue3 component that includes a slot with Cypress

I'm currently facing challenges setting up a new project. The technologies I am using include Vue3, TypeScript, and Cypress. It seems like the problem lies within the TypeScript configuration. Below is a Minimal Working Example (MWE) of my setup. Any ...

Mastering the art of theming components using styled-components and Material-UI

Can you integrate the Material-UI "theme"-prop with styled-components using TypeScript? Here is an example of Material-UI code: const useStyles = makeStyles((theme: Theme) => ({ root: { background: theme.palette.primary.main, }, })); I attemp ...

Error encountered in Storybook: The value is not iterable (property Symbol(Symbol.iterator) cannot be read)

I recently created a React library using and opted for the React + TypeScript + Storybook template. You can find the entire codebase here → https://github.com/deadcoder0904/react-typical I encountered the following error: undefined is not iterable ( ...

Seamless database migrations using sequelize and typescript

I've been exploring the concept of generating migration files for models that already exist. When I use the "force: true" mode, tables are automatically created in the database, so I find it hard to believe that creating migration files automatically ...

The directive for angular digits only may still permit certain characters to be entered

During my exploration of implementing a digits-only directive, I came across a solution similar to my own on the internet: import { Directive, ElementRef, HostListener } from '@angular/core'; @Directive({ selector: '[appOnlyDigits]' ...

How to effectively implement React Context with the useState hook in a TypeScript project

I've been working with code that resembles something like the following: SomeContext.ts: export interface SomeContext { someValue: string; someFunction: () => void; } export const defaultValue: SomeContext = { someValue: "", som ...

Encountering errors in Typescript build due to issues in the node_modules directory

While running a typescript build, I encountered errors in the node_modules folder. Despite having it listed in the exclude section of my tsconfig.json file, the errors persist. What's puzzling is that another project with identical gulpfile.js, tsconf ...

How to extract key-value pairs from an object in a TypeScript API request

I am trying to extract the data '"Cursed Body": "100.000%"' from this API response using TypeScript in order to display it on an HTML page. Can anyone help me figure out how to do this? API Response { "tier": &q ...

What could be causing my Vue code to behave differently than anticipated?

There are a pair of components within the div. When both components are rendered together, clicking the button switches properly. However, when only one component is rendered, the switch behaves abnormally. Below is the code snippet: Base.vue <templa ...

What is the best way to restrict the maximum number of items stored in local storage?

I'm creating a GitHub search app using the GitHub API in Angular. I want to restrict the number of items that can be stored in local storage. If the number of stored elements exceeds 5, the "Add to Favorite" button should either stop working or disapp ...

Reusing a lazy-loaded module across multiple applications

Currently, I am working on an enterprise Angular 2 application with numerous lazy loaded modules. A new project came up where I needed to reuse a module that was previously created for the main app. After researching online, the only solution I found was ...

"Trouble with Typescript's 'keyof' not recognizing 'any' as a constraint

These are the current definitions I have on hand: interface Action<T extends string, P> { type: T; payload: P; } type ActionDefinitions = { setNumber: number; setString: string; } type ActionCreator<A extends keyof ActionDefinitions> ...

Errors occur with Metro bundler while utilizing module-resolver

Recently, I completed a project using the expo typescript template that runs on both iOS and Android platforms, excluding web. To enhance my development process, I established path aliases in the tsconfig.json file as shown below: "paths": { "@models/ ...

Creating Object of Objects in TypeScript: A Comprehensive Guide

Assuming I have a structure similar to this: interface Student { firstName: string; lastName: string; year: number; id: number; } If I intend to handle an array of these structures, I can simply specify the type as Student[]. Instead of utilizin ...

React: Implement a feature to execute a function only after the user finishes typing

Currently, I am using react-select with an asynchronous create table and have integrated it into a Netsuite custom page. A issue I am facing is that I would like the getAsyncOptions function to only trigger when the user stops typing. The problem right now ...