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

Limitations on quantity utilizing typescript

Looking to create a type/interface with generics that has two properties: interface Response<T> { status: number; data: T | undefined; } Specifically, I want to enforce a rule where if the status is not equal to 200, then data must be undefined. ...

What is the maximum number of groupings that can be created from a set of numbers within a

I'm trying to figure out how to handle a specific task, but I'm running into some obstacles. When adding numbers to clusters, a number is considered to belong to a cluster if its distance to at least one existing number in the cluster is within a ...

Initiate and terminate server using supertest

I've developed a server class that looks like this: import express, { Request, Response } from 'express'; export default class Server { server: any; exp: any; constructor() { this.exp = express(); this.exp.get('/' ...

Steps for assigning the TAB key functionality within a text area

Is there a way to capture the TAB key press within a text area, allowing for indentation of text when the user uses it? ...

Tips on searching for an entry in a database with TypeScript union types when the type is either a string or an array of strings

When calling the sendEmail method, emails can be sent to either a single user or multiple users (with the variable type string | string[]). I'm trying to find a more efficient and cleaner way to distinguish between the two in order to search for them ...

Using Angular 5 to link date input to form field (reactive approach)

I'm encountering an issue with the input type date. I am trying to bind data from a component. Below is my field: <div class="col-md-6"> <label for="dateOfReport">Data zgłoszenia błędu:</label> <input type="date" formC ...

Prevent updating components when modifying state variables

Introduction I've developed a React component that consists of two nested components. One is a chart (created with react-charts) and the other is a basic input field. Initially, I have set the input field to be hidden, but it becomes visible when the ...

What methods can I use to combine existing types and create a brand new one?

Is there a way to combine existing types to create a new type in TypeScript? `export type Align = 'center' | 'left' | 'right' export type Breakpoints = ‘sm’ | ‘md’` I want to merge the Align and Breakpoints types to ...

Exploring the use of global variables in React

Welcome to my React learning journey! I've encountered an issue while trying to access a global variable exposed by a browser extension that I'm using. Initially, I attempted to check for the availability of the variable in the componentDidMount ...

When using TypeORM, make sure to include the "WHERE IN (...)" clause in the query condition only if there is a value associated with it

In my TypeScript node.js project using TypeORM (v0.2.40), I have a query to find a record in the database based on specific criteria: userRepository.find({ where: { firstName: 'John', company: 'foo' } }); This executes the following SQ ...

Adjusting the selection in the Dropdown Box

I've been attempting to assign a value to the select box as shown below: <dx-select-box [items]="reportingProject" id="ReportingProj" [text]="reportingProject" [readOnly]="true" > ...

Flashing issues when utilizing the Jquery ui slider within an Angular 2 component

I recently incorporated a jquery-ui slider plugin into an angular 2 component and it's been working well overall, but I have encountered an annoying issue. Whenever the slider is used, there is a flickering effect on the screen. Interestingly, when I ...

Namespace remains ambiguous following compilation

I'm currently developing a game engine in TypeScript, but I encountered an issue when compiling it to JavaScript. Surprisingly, the compilation process itself did not throw any errors. The problem arises in my main entry file (main.ts) with these ini ...

unable to transform this string into an object

https://i.sstatic.net/O46IL.pngWhy am I encountering difficulties converting this string into an object? Any assistance on resolving this error would be greatly appreciated. onSignup(data:any){ localStorage.setItem('users',JSON.string ...

Unraveling the mysteries of webpack configuration

import * as webpack from 'webpack'; ... transforms.webpackConfiguration = (config: webpack.Configuration) => { patchWebpackConfig(config, options); While reviewing code within an Angular project, I came across the snippet above. One part ...

Using TypeScript with Node.js: the module is declaring a component locally, but it is not being exported

Within my nodeJS application, I have organized a models and seeders folder. One of the files within this structure is address.model.ts where I have defined the following schema: export {}; const mongoose = require('mongoose'); const addressS ...

The 'style' property is not found within the 'EventTarget' type

Currently, I am utilizing Vue and TypeScript in an attempt to adjust the style of an element. let changeStyle = (event: MouseEvent) => { if (event.target) { event.target.style.opacity = 1; Although the code is functional, TypeScript consist ...

Searching and adding new elements to a sorted array of objects using binary insertion algorithm

I'm currently working on implementing a method to insert an object into a sorted array using binary search to determine the correct index for the new object. You can view the code on codesanbox The array I have is sorted using the following comparis ...

Guide on Linking a Variable to $scope in Angular 2

Struggling to find up-to-date Angular 2 syntax is a challenge. So, how can we properly connect variables (whether simple or objects) in Angular now that the concept of controller $scope has evolved? import {Component} from '@angular/core' @Comp ...

Underscore.js is failing to accurately filter out duplicates with _uniq

Currently, I am integrating underscorejs into my angular project to eliminate duplicate objects in an array. However, I have encountered an issue where only two string arrays are being kept at a time in biddingGroup. When someone else places a bid that is ...