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

Is Angular UI's data binding more of a push or pull mechanism? How can I optimize its speed?

Suppose I have a variable a that is displayed in HTML as {{a}}. If I then update its value in TypeScript using a = "new value";, how quickly will the new value be reflected in the user interface? Is there a mechanism that periodically checks all bound var ...

Function that yields promise result

I need help figuring out how to make this recursive function return a promise value. I've attempted various approaches, but they all resulted in the search variable ending up as undefined. public search(message: Message) { let searchResult: strin ...

Attempting to create a function that can accept two out of three different types of arguments

I am trying to create a function that only accepts one of three different types type A = 'a' type B = 'b' type C = 'c' The function should accept either type A, C, or both B and C, but not all three types. This is what I hav ...

Creating a JSON object from an array of data using TypeScript

This might not be the most popular question, but I'm asking for educational purposes... Here is my current setup: data = {COLUMN1: "DATA1", COLUMN2: "DATA2", COLUMN3: "DATA3", ..., COLUMNn: "DATAn"}; keyColumns = ["COLUMN2", "COLUMN5", "COLUMN9"]; ...

What is the best way to filter an array of objects and store the results in a new variable list using typescript?

On the lookout for male objects in this list. list=[ { id: 1, name: "Sam", sex: "M"}, { id: 2, name: "Jane", sex: "F"}, { id: 3, name: "Mark", sex: "M"}, { id: 4, name: "Mary, sex: "F& ...

Switching cell icon when clicked - A step-by-step guide

I have a situation in ag-grid where I need to update the icon of a button in a cell when it is clicked to indicate progress and then revert back to its original state upon completion of the action. Below is the code snippet: my-custom.component.ts < ...

Issue customizing static method of a subclass from base class

Let me illustrate a simplified example of my current code: enum Type {A, B} class Base<T extends Type> { constructor(public type: T) {} static create<T extends Type>(type: T): Base<T> { return new Base(type); } } class A exte ...

Typescript combined with MongoDB models

One common issue I have encountered involves a method used on my repository: async findByEmail(email: string): Promise<User | null> { const user = await UserModel.findOne({ email }); if(!user) return null; ...

Excluding node modules from Webpack TerserPlugin

I am currently working on a custom Angular Builder and I need to exclude an entire module from the minification/optimization process. According to the Webpack .md file: exclude Type: String|RegExp|Array Default: undefined This setting is used to spe ...

How to create an array of objects using an object

I have a specific object structure: { name: 'ABC', age: 12, timing: '2021-12-30T11:12:34.033Z' } My goal is to create an array of objects for each key in the above object, formatted like this: [ { fieldName: 'nam ...

How can you toggle the selection of a clicked element on and off?

I am struggling with the selection color of my headings which include Administration, Market, DTA. https://i.stack.imgur.com/luqeP.png The issue is that the selection color stays on Administration, even when a user clicks on another heading like Market. ...

When attempting to open an Angular modal window that contains a Radio Button group, an error may occur with the message "ExpressionChanged

I am brand new to Angular and have been trying to grasp the concept of lifecycle hooks, but it seems like I'm missing something. In my current project, there is a Radio Button Group nested inside a modal window. This modal is triggered by a button cl ...

Ensure that missing types are included in a union type following a boolean evaluation

When working with typescript, the following code will be typed correctly: let v: number | null | undefined; if(v === null || v === undefined) return; // v is now recognized as a `number` const v2 = v + 2; However, if we decide to streamline this process ...

Troubleshooting Next.js 14 JWT Session Error in Conjunction with Next Auth - addressing a type

Recently, I delved into working with Next.js 14 and Next Auth 5 beta. However, every time I attempt to log in, I encounter the following error: [auth][error][JWTSessionError] [auth][cause]: TypeError: Cannot read properties of undefined (reading 'user ...

When attempting to add mp3 files to a Vue/TypeScript application, a "Module not found" error is triggered

I am encountering an error while trying to import .mp3 files into my Vue/Typescript app. Upon running 'npm serve', I am presented with the following message: ERROR in /Users/***/***/application/src/components/Sampler.vue(80,16): 80:16 Cannot fin ...

The redirection code is not being executed when calling .pipe() before .subscribe()

My AuthService has the following methods: signUp = (data: SignUp): Observable<AuthResponseData> => { const endpoint = `${env.authBaseUrl}:signUp?key=${env.firebaseKey}`; return this._signInOrSignUp(endpoint, data); }; signIn = (data: SignIn): ...

Accurate linking to the interface while retrieving information from a specified URL

Just started with Angular and attempting to assign the returned json data to my interface, but it's not working as expected. Check out the code I'm using below: Stackblitz Json URL ...

Require assistance with accurately inputting a function parameter

I developed this function specifically for embedding SVGs export function svgLoader( path: string, targetObj: ElementRef ){ let graphic = new XMLHttpRequest; graphic.open('GET', path, !0), graphic.send(), graphic.onload = (a)=> ...

Using Bazel, Angular, and SocketIO Version 3 seems to be triggering an error: Uncaught TypeError - XMLHttpRequest is not recognized

Looking to integrate socket.io-client (v3) into my Angular project using Bazel for building and running. Encountering an error in the browser console with the ts_devserver: ERROR Error: Uncaught (in promise): TypeError: XMLHttpRequest is not a constructor ...

having difficulties sorting a react table

This is the complete component code snippet: import { ColumnDef, flexRender, SortingState, useReactTable, getCoreRowModel, } from "@tanstack/react-table"; import { useIntersectionObserver } from "@/hooks"; import { Box, Fl ...