Secure method of utilizing key remapped combined type of functions

Imagine having a union type called Action, which is discriminated on a single field @type, defined as follows:

interface Sum {
    '@type': 'sum'
    a: number
    b: number
}

interface Square {
    '@type': 'square'
    n: number
}

type Action = Sum | Square

Now, let's create an object with methods for each Action using key remapping:

const handlers: {[A in Action as A['@type']]: (action: A) => number} = {
    sum: ({a, b}) => a + b,
    square: ({n}) => n ** 2
}

Is there a safe way to call one of these handlers by passing the Action as an argument? The goal is to ensure type safety in the following code snippet:

const runAction = (action: Action) => {
    return handlers[action['@type']](action) // <-- Not valid
}

However, TypeScript throws an error stating:

Argument of type 'Action' is not assignable to parameter of type 'never'.
  The intersection 'Sum & Square' was reduced to 'never' because property ''@type'' has conflicting types in some constituents.
    Type 'Sum' is not assignable to type 'never'.

What would be a proper way to rewrite the above function in a safe manner without sacrificing strict typing?

Playground link

Answer №1

The primary concern at hand lies in the fact that both handlers[action['@type']] and action have union types, but their values are interrelated in a manner not captured by these independent union types. The compiler lacks the understanding that, for instance, passing a Square into (action: Sum) => number will never happen. This underlying issue is discussed in microsoft/TypeScript#30581.

It's important to note that the compiler does not perform control flow analysis across unions. Specifically, when analyzing:

handlers[action['@type']](action)

the compiler does not engage in human-like reasoning where it would infer which type of action is being used and act accordingly. Therefore, adding extra analysis based on each union member to a line of code becomes impractical due to the exponentially growing number of cases in larger unions. A previous request to include such analysis on an as-needed basis was declined (see microsoft/TypeScript#25051). So, there isn't a straightforward solution like appending as if switch(action) to the end of the line.


Prior to TypeScript 4.6, options were limited - one could either sacrifice type safety by using type assertions, or resort to redundant code to force multiple-pass analysis. However, with the release of TypeScript 4.6 and beyond, the introduction of microsoft/TypeScript#47109 offers a method to refactor code with correlated unions into a form that ensures compiler verification of safety through utilities like generics and distributive object types.

type ActionMap = { [A in Sum | Square as A['@type']]: A }

type Action<K extends keyof ActionMap = keyof ActionMap> =
    { [P in K]: ActionMap[P] & { "type": P } }[K]

const runAction = <K extends keyof ActionMap>(action: Action<K>) => {
    return handlers[action['@type']](action)
}

In this approach, we define an ActionMap type followed by a generic Action<K>. The refactored runAction() function is now capable of ensuring type safety without repetitive coding needed earlier.

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

Enable a fraction of a category

Imagine having a structure like this interface User { name: string; email: string; } along with a function like this updateUser(user: User) { } As currently defined, updateUser cannot accept only a name (updateUser({name: 'Anna'} would fa ...

Display the ion-button if the *ngIf condition is not present

I am working with an array of cards that contain download buttons. My goal is to hide the download button in the first div if I have already downloaded and stored the data in the database, and then display the second div. <ion-card *ngFor="let data o ...

When inserting a child element before the myArray.map(x => ) function, it results in rendering only a single child element from the array

Sorry for the confusion in my explanation, but I'm encountering an issue with displaying elements from an array. Here is the code snippet I am working on. Currently, the myArray contains 10 elements. When I place the <LeadingChild/> component ...

Troubleshooting History.push issue in a Typescript and React project

Currently, I'm tackling a project using React and TypeScript, but I've encountered a problem. Whenever I attempt to execute a history.push function, it throws an error that reads: Uncaught (in promise) TypeError: history.push is not a function. ...

Is there a way to identify and retrieve both the initial and final elements in React?

Having an issue with logging in and need to retrieve the first and last element: Below is my array of objects: 0: pointAmountMax: 99999 pointAmountMin: 1075 rateCode: ['INAINOW'] roomPoolCode: "ZZAO" [[Prototype]]: Object 1: pointAmoun ...

Strange behavior when working with Typescript decorators and Object.defineProperty

I'm currently working on a project that involves creating a decorator to override a property and define a hidden property. Let's take a look at the following example: function customDecorator() { return (target: any, key: string) => { ...

Can one invoke ConfirmationService from a different Service?

How can I declare an application-wide PrimeNG dialog and display it by calling ConfirmationService.confirm() from another service? Below is the HTML code in app.component.html: <p-confirmDialog [key]="mainDialog" class="styleDialog" ...

NestJS is having trouble importing generated types from the Prisma client

When working with Prisma in conjunction with NestJs, I encountered an issue after defining my model and generating it using npx prisma generate. Upon importing the generated type, I can easily infer its structure: import { FulfilmentReport, FulfilmentRepor ...

Utilizing the combineReducers() function yields disparate runtime outcomes compared to using a single reducer

Trying to set up a basic store using a root reducer and initial state. The root reducer is as follows: import Entity from "../api/Entity"; import { UPDATE_GROUPING } from "../constants/action-types"; import IAction from "../interfaces/IAction"; import IS ...

How can I implement a scroll bar in Angular?

I am facing an issue with my dialog box where the expansion panel on the left side of the column is causing Item 3 to go missing or appear underneath the left column when I expand the last header. I am looking for a solution to add a scroll bar so that it ...

Create a single datetime object in Ionic (Angular) by merging the date and time into a combined string format

I have a string in an ionic project that contains both a date and time, and I need to merge them into a single datetime object for sending it to the server const date = "Fri Jan 07 2022 13:15:40 GMT+0530 (India Standard Time)"; const time = " ...

Utilizing useContext data in conjunction with properties

When using useContext in a component page, I am able to successfully retrieve data through useContext within a property. customColorContext.js import { createContext, useEffect, useState, useContext } from 'react'; // creating the context objec ...

Tips for setting variable values in Angular 7

I'm encountering an issue with assigning values to variables in my code. Can anyone provide assistance in finding a solution? Here is the snippet of my code: app.component.ts: public power:any; public ice:any; public cake:any; changeValue(prop, ...

An easy way to insert a horizontal line between your text

Currently, I have two text responses from my backend and I'm considering how to format them as shown in the design below. Is it possible to automatically add a horizontal line to separate the texts if there are two or more broadcasts instead of displa ...

Setting up pagination in Angular Material can sometimes present challenges

After implementing pagination and following the guidelines provided here. This is my code from the app.component.ts file - import { Component, OnInit, ViewChild } from '@angular/core'; import {MatPaginator} from '@angular/material/paginat ...

Confirm that a new class has been assigned to an element

I'm having trouble creating a unit test for a Vue.js component where I need to check if a specific CSS class is added to the template. Below is the template code: <template> <div id="item-list" class="item-list"> <table id="item ...

What might be causing AngularJS to fail to display values when using TypeScript?

I have designed the layout for my introduction page in a file called introduction.html. <div ng-controller="IntroductionCtrl"> <h1>{{hello}}</h1> <h2>{{title}}</h2> </div> The controller responsible for handling th ...

Having trouble managing TypeScript in conjunction with React and Redux

As a newcomer to TypeScript, I find myself struggling to grasp the concepts and know where to start or stop. While there are abundant resources available online, I have not been able to effectively utilize them in my project. I am hopeful for some guidance ...

Exploring TypeScript Object Properties in Angular 2

How can I extract and display the ID and title of the Hero object below? The structure of the Hero interface is based on a Firebase JSON response. hero.component.ts import {Component, Input} from 'angular2/core'; import {Hero} from '../mod ...

How can I combine multiple styles using Material-UI themes in TypeScript?

There are two different styles implementations in my code. The first one is located in global.ts: const globalStyles = (theme: Theme) => { return { g: { marginRight: theme.spacing(40), }, } } export const mergedStyle = (params: any) ...