Depend on a mapping function to assign a value to every option within a discriminated union

While utilizing all variations of a discriminated union with conditional if statements in TypeScript, the type is narrowed down to the specific variant. To achieve the same effect by expressing the logic through a mapping from the discriminant to a function that processes the variant, it's essential to view the mapping as a distributive type rather than just a mapped type. Despite reading through resources like this Stack Overflow answer and this GitHub pull request, I'm still struggling to apply this concept to my own scenario.

type Message =
  | { kind: "mood"; isHappy: boolean }
  | { kind: "age"; value: number };

// Mapping structure example for reference
type Mapping = {
  mood: (payload: { isHappy: boolean }) => void;
  age: (payload: { value: number }) => void;
};

// Sample mapping object ensuring correct keys and signatures
const mapping: Mapping = {
  mood: ({ isHappy }) => {
    console.log(isHappy);
  },
  age: ({ value }) => {
    console.log(value + 1);
  },
};

// Function to process the message based on its kind
const process = (message: Message) => {
  // Issue arises here as TypeScript cannot infer the right message type
  mapping[message.kind](message);
};

Answer №1

The process of restructuring discussed in microsoft/TypeScript#47109 encourages the utilization of a simple "basic" object type like

interface Mapping {
  mood: {
    isHappy: boolean;
  };
  age: {
    value: number;
  };
}

This can be derived from your original Message type by transforming it into:

type _Message =
  | { kind: "mood"; isHappy: boolean }
  | { kind: "age"; value: number };

type Mapping = { [T in _Message as T["kind"]]: 
  { [K in keyof T as Exclude<K, "kind">]: T[K] } 
}

Other types should either utilize mapped types on that object type, for example:

type MappingCallbacks =
  { [K in keyof Mapping]: (payload: Mapping[K]) => void }

const mappingCallbacks: MappingCallbacks = {
  mood: ({ isHappy }) => {
    console.log(isHappy);
  },
  age: ({ value }) => {
    console.log(value + 1);
  },
};

or employ distributive object types which are mapped types that immediately expand into a union, such as:

type Message<K extends keyof Mapping = keyof Mapping> =
  { [P in K]: { kind: P } & Mapping[P] }[K]

type M = Message
/* type M = 
     ({ kind: "mood"; } & { isHappy: boolean; }) | 
     ({ kind: "age"; } & { value: number; }) 
*/

Therefore, your operations must embrace generics with respect to the key type of your basic interface:

const process = <K extends keyof Mapping>(message: Message<K>) => {
  mappingCallbacks[message.kind](message);
};

While the non-generic version of process may seem conceptually viable, where message is just a union, the compiler struggles to acknowledge that each member of the union complies with a similar constraint. This issue was addressed in microsoft/TypeScript#30581 and subsequently resolved in microsoft/TypeScript#47109.

Link to Play around with the code

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

The function cannot be accessed during the unit test

I have just created a new project in VueJS and incorporated TypeScript into it. Below is my component along with some testing methods: <template> <div></div> </template> <script lang="ts"> import { Component, Vue } from ...

Establish an enumeration using universally recognized identifiers

I have a JavaScript function that requires a numerical input, as well as some predefined constants at the top level: var FOO = 1; var BAR = 2; The function should only be called using one of these constants. To ensure type safety in TypeScript, I am att ...

Creating a custom decision tree in Angular/JS/TypeScript: A step-by-step guide

My current project involves designing a user interface that enables users to develop a decision tree through drag-and-drop functionality. I am considering utilizing GoJS, as showcased in this sample: GoJS IVR Tree. However, I am facing challenges in figuri ...

Tips for utilizing the 'crypto' module within Angular2?

After running the command for module installation: npm install --save crypto I attempted to import it into my component like this: import { createHmac } from "crypto"; However, I encountered the following error: ERROR in -------------- (4,28): Canno ...

Is there a way to simultaneously call two APIs and then immediately call a third one using RXJS?

I am looking to optimize the process of making API calls by running two in parallel and then a third immediately after both have completed. I have successfully implemented parallel API calls using mergeMap and consecutive calls using concatMap, but now I w ...

Responsive Container MUI containing a grid

I am attempting to replicate the functionality seen on YouTube's website, where they dynamically adjust the grid layout based on the container size when a Drawer is opened or closed. Essentially, it seems that YT adjusts the grid count based on the c ...

encountering issues with configuring TypeScript LSP in NeoVim with the use of the lazy package manager

Encountered an error in nvim when opening a .ts file. Using mason, mason-lspconfig, and nvim-lspconfig for lsp setup. Lua language lsp is functioning properly, but facing errors with ts files as shown in the screenshot below: https://i.stack.imgur.com/gYM ...

Is there another option for addressing this issue - A function that does not declare a type of 'void' or 'any' must have a return value?

When using Observable to retrieve data from Http endpoints, I encountered an error message stating that "A function whose declared type is neither 'void' nor 'any' must return a value." The issue arises when I add a return statement in ...

I am encountering an issue where my application is not recognizing the angular material/dialog module. What steps can I take to resolve this issue and ensure that it functions properly?

For my Angular application, I am trying to incorporate two Material UI components - MatDialog and MatDialogConfig. However, it seems like there might be an issue with the placement of these modules as all other modules are functioning correctly except fo ...

Issue locating name (generics) in Angular 4

I'm encountering issues with Angular 4. The TypeScript code is not compiling and generating errors when I try to run ng serve. I'm getting two errors in my-data.service.ts file - "Cannot find name 'Category' at line 9, column 30" and ...

You are unable to link to <custom directive selector> because it is not recognized as a valid property of 'div'

I am currently working on a project in StackBlitz, and you can find the link here: https://stackblitz.com/edit/angular-fxfo3f?file=src/directives/smooth-height.directive.ts I encountered an issue: Error in src/components/parent/parent.component.html (2:6) ...

Ways to address the issue arising from the utilization of the "as" keyword

Every time I encounter this issue - why must I always provide all the details? type Document = Record<string, any> type FilteredDocument<T extends Document> = {[key in keyof T as T[key] extends (()=>void) ? never : key]: T[key]} const ...

The issue with Rxjs forkJoin not being triggered within an Angular Route Guard

I developed a user permission service that retrieves permissions from the server for a specific user. Additionally, I constructed a route guard that utilizes this service to validate whether the user possesses all the permissions specified in the route. To ...

Leveraging npm for the development of my TypeScript/Node.js project

I'm facing challenges working on a project written in TypeScript and running on Node. I am finding it difficult to write the npm script to get it up and running properly for development purposes. What I am attempting to achieve is: clear the /dist f ...

The specified type 'x' cannot be assigned to the type 'x'. Error code: 2322

I encountered an issue with the code in @/components/ui/billboard.tsx file import { Billboard } from "@/types" interface BillboardProps { data: Billboard; }; const BillboardComponent: React.FC<BillboardProps> = ({ data }) => ...

Retrieve the attribute from the element that is in the active state

I'm facing a challenge in determining the active status of an element attribute. I attempted the following approach, but it incorrectly returned false even though the element had the attribute in an active state - (.c-banner.active is present) During ...

Issues with Injectable Service within Another Service in Angular 2

Having a problem with injecting a service into another service. I have a ContactService that retrieves data from the server using the handleError method, and an AlertService that handles errors thrown from the handleError method. Both services are declared ...

Jasmine encountered an error while trying to compare the same string: 'Expected the values to match.'

I'm encountering an error message, despite verifying that the strings are identical: Expected { $$state : { status : 1, value : { customerNumber : 'customerNumber', name : 'name', userId : 'buId', customerType : 'ty ...

Having difficulties incorporating a separate library into an Angular project

My typescript library contains the following code, inspired by this singleton example code export class CodeLib { private static _instance: CodeLib; constructor() { } static get instance(): CodeLib { if(!this._instance){ ...

Detecting changes in a readonly input in Angular 4

Here is a code snippet where I have a readonly input field. I am attempting to change the value of this readonly input from a TypeScript file, however, I am encountering difficulty in detecting any changes from any function. See the example below: <inp ...