Exploring TypeScript's type discrimination with objects

When working with TypeScript, type discrimination is a powerful concept:

https://www.typescriptlang.org/play#example/discriminate-types

But is it possible to achieve something like this?

type A = {id: "a", value: boolean};
type B = {id: "b", value: string};

type AB = A | B;

const Configs = {
  a: {
    renderer: (data: A)=>{
      // ...
    }
  },
  b: {
    renderer: (data: B)=>{
      // ...
    }
  }
} as const;

const renderElement = (element: AB)=>{
  const config = Configs[element.id];

  const renderer = config.renderer;
  return renderer(element); 
}

My code fails at the last line with the following error message. I have an idea of what it means but unsure how to resolve it.

Argument of type 'AB' is not assignable to parameter of type 'never'.
  The intersection 'A & B' was reduced to 'never' due to conflicting types in property 'id' among constituents.
    Type 'A' cannot be assigned to type 'never'.(2345)

Here's a longer example showcasing what I'm currently working on (not fully tested and might have incorrect recursive typing).

type HeaderProps = { text: string, size: number, color: string};
type ImageProps = { src: string, width: number, height: string};
type GroupProps = { numberOfColumns: number, children: Item[], height: string};

type Item = {
  kind: string,
  data: HeaderProps | ImageProps | GroupProps
}

const itemsConfig = {
  header: {
    onRender: (props: HeaderProps)=>{
      
    }
    // + some other handlers...
  },
  image: {
    onRender: (props: ImageProps)=>{
      
    }
    // + some other handlers...
  },
  group: {
    onRender: (props: GroupProps)=>{

    }
    // + some other handlers...
  }
} as const;

const exampleItemsPartA = [
  {
    kind: 'header',
    data: {
      text: 'Hello world',
      size: 16,
    }
  },
  {
    kind: 'header',
    data: {
      text: 'Hello world smaller',
      size: 13,
      color: 'red'
    }
  },
  {
    kind: 'image',
    data: {
      src: 'puppy.png',
      width: 100,
      height: 100,
    }
  }
];

const exampleItems = [
  ...exampleItemsPartA,
  {
    kind: 'group',
    data: exampleItemsPartA
  }
]

const renderItems = (items: Item[])=>{
  const result = [];

  for(const item of items){
    const {onRender} = itemsConfig[item.kind];
    result.push(onRender(item.data))
  }

  return result;
}

Answer №1

When considering both your original and expanded examples, the objective is to address a concept referred to as a "correlated union type," highlighted in discussions on union types and elaborated on in conversations pertaining to microsoft/TypeScript#30581.

In the context of the renderElement function, there exists a situation where the renderer value encompasses a union of functions, while the element value encapsulates a union of function arguments. However, the compiler faces challenges when trying to reconcile these unions in correlation with one another. It fails to acknowledge that if element is of type A, then renderer should be capable of accepting a value of type A. Consequently, it interprets any invocation of renderer() as invoking a union of functions, enforcing restrictions for acceptable arguments to cater to every function type within the union – an entity embodying characteristics of being both an A and a B, akin to the notion of an intersection A & B, which ultimately results in a paradoxical scenario reducing to never due to the impossibility of aligning both A and B:

const renderElement = (element: AB) => {
  const config = Configs[element.id];     
  const renderer = config.renderer;
  /* const renderer: ((data: A) => void) | ((data: B) => void) */
  return renderer(element); // error, element is not never (A & B)
}

To navigate around this limitation, employing a type assertion emerges as the most expedient approach, signaling to the compiler to forgo verifying type safety concerns:

return (renderer as (data: AB) => void)(element); // okay

This operation essentially reassures the compiler that renderer can indeed accept either A or B, regardless of what the caller chooses to pass in. While misleading, this method remains harmless since the expectation is that element will inevitably match the type anticipated by renderer.


Traditionally, resorting to such tactics marked the culmination of overcoming this obstacle. However, recent developments surrounding microsoft/TypeScript#47109 herald a potential solution offering type-safe correlated unions. This enhancement has been integrated into the primary TypeScript codebase's main branch, implying its imminent inclusion in the forthcoming TypeScript 4.6 release. Developers keen on exploring this functionality beforehand can leverage nightly typescript@next builds to preview its capabilities.

The revised example below elucidates how one could implement the fix within your initial code snippet. To commence, we establish an object type delineating the nexus between the discriminant values of A and B alongside their respective data types:

type TypeMap = { a: boolean, b: string };

Subsequently, we proceed to define A, B, and AB drawing upon the foundation laid by TypeMap:

type AB<K extends keyof TypeMap = keyof TypeMap> = 
  { [P in K]: { id: P, value: TypeMap[P] } }[K];

This construct is commonly referred to as a "distributive object type," whereby the generic type parameter K, delimited by the discriminant values, undergoes subdivision into individual members denoted by P, facilitating the distribution of the operation {id: P, value: TypeMap[P]} across said union.

Validating the viability of this approach:

type A = AB<"a">; // type A = { id: "a"; value: boolean; }
type B = AB<"b"> // type B = { id: "b"; value: string; }
type ABItself = AB // { id: "a"; value: boolean; } | { id: "b"; value: string; }

(Note that utilizing AB sans a type parameter defaults to keyof TypeMap, denoting the union "a" | "b".)

Concurrently, for configs, explicit annotation in alignment with a similarly mapped type becomes imperative, transforming

TypeMap</code into a variant encompassing properties where each <code>K
property incorporates a renderer attribute serving as a function receptive to AB<K>:

const configs: { [K in keyof TypeMap]: { renderer: (data: AB<K>) => void } } = {
  a: { renderer: (data: A) => { } },
  b: { renderer: (data: B) => { } }
};

This annotated declaration holds cardinal importance, enabling the compiler to correlate AB<K> with configs. By establishing renderElement as a generic function contingent on K, the invocation now proceeds seamlessly owing to the mutual recognition wherein a function accommodating AB<K> invariably accepts a value mirroring AB<K>:

const renderElement = <K extends keyof TypeMap>(element: AB<K>) => {
  const config = configs[element.id];
  const renderer = config.renderer;
  return renderer(element); // okay
}

With no lingering errors, you should effortlessly invoke renderElement allowing the compiler to infer

K</code based on the input provided:</p>
<pre><code>renderElement({ id: "a", value: true });
// const renderElement: <"a">(element: { id: "a"; value: boolean; }) => void
renderElement({ id: "b", value: "okay" });
// const renderElement: <"b">(element: { id: "b"; value: string; }) => void

Hence, the crux of the matter elucidates the efficacy behind leveraging these strategies effectively, ensuring smooth navigation through scenarios necessitating precision alignment within correlated unions.

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 it advisable for a component to handle the states of its sub-components within the ngrx/store framework?

I am currently grappling with the best strategy for managing state in my application. Specifically, whether it makes sense for the parent component to handle the state for two subcomponents. For instance: <div> <subcomponent-one> *ngIf=&qu ...

Choose does not showcase the updated value

My form contains a form control for currency selection Each currency object has the properties {id: string; symbol: string}; Upon initialization, the currency select component loops through an array of currencies; After meeting a specific condition, I need ...

Building a Vuetify Form using a custom template design

My goal is to create a form using data from a JSON object. The JSON data is stored in a settings[] object retrieved through an axios request: [ { "id" : 2, "name" : "CAR_NETWORK", "value" : 1.00 }, { "id" : 3, "name" : "SALES_FCT_SKU_MAX", "val ...

Transferring information to a view using ExpressJS and Reactjs

I have created an application that requires users to log in with Twitter credentials. Once logged in successfully, I typically pass the user data to the template using the following code: app.get('/', function(req, res, next) { log("The ...

Retrieving POST data from requests in Node.js

My goal is to extract parameters from a POST request and store them in the variable postData using the request module. I found helpful information on handling post requests with Express.js here. Additionally, I came across this useful thread on how to retr ...

Experiencing a 404 ERROR while attempting to submit an API POST request for a Hubspot form within a Next.js application

Currently, I am in the process of developing a Hubspot email submission form using nextjs and typescript. However, I am encountering a couple of errors that I need help with. The first error pertains to my 'response' constant, which is declared b ...

Problem importing npm module with Meteor 1.3

I've been trying to follow the guide for using npm packages in Meteor 1.3, particularly focusing on installing the moment npm module. However, I can't seem to work it out despite its simplicity. Every time I attempt to use the package in the cli ...

Making a quick stop in Istanbul and NYC to collect a few important files

Setting up Istanbul/Nyc/Mocha for test coverage in my project has been a bit of a challenge. While I was successful in running Nyc, I noticed that not all the .ts files in my project were being picked up for test coverage. When I execute npm run coverag ...

Combine arrays using union or intersection to generate a new array

Seeking a solution in Angular 7 for a problem involving the creation of a function that operates on two arrays of objects. The goal is to generate a third array based on the first and second arrays. The structure of the third array closely resembles the f ...

Best Practices for Making Remote Requests in Ruby on Rails

Currently, I am creating a Single Page Application using Ruby on Rails for the first time. Although I am still learning, I have set up a side menu with links and a container on the right part of the page to display partial pages. An example of one of my me ...

Detecting Changes in Angular Only Works Once when Dealing with File Input Fields

Issue arises with the file input field as it only allows uploading one file at a time, which needs to be modified. Uploading a single file works fine. However, upon attempting to upload multiple files, it appears that the "change" handler method is not tr ...

Integration of a QR code scanner on a WordPress website page

I'm in the process of setting up a QR code scanner on my Wordpress site or within a popup. The goal is for users to be able to scan a QR code when they visit the page/popup link. Specifically, the QR code will represent a WooCommerce product URL, and ...

ES6 module import import does not work with Connect-flash

Seeking assistance with setting up connect-flash for my nodejs express app. My goal is to display a flashed message when users visit specific pages. Utilizing ES6 package module type in this project, my code snippet is as follows. No errors are logged in t ...

How to efficiently store and manage a many-to-many relationship in PostgreSQL with TypeORM

I have a products entity defined as follows: @Entity('products') export class productsEntity extends BaseEntity{ @PrimaryGeneratedColumn() id: number; //..columns @ManyToMany( type => Categories, categoryEntity => cat ...

Utilizing Express Routes to specify React page components

Currently, as I delve into the world of learning Express.js, I find myself faced with a unique scenario involving 2 specific URLs: /security/1 /security/2 As per the requirements of these URLs, the expected response will vary. If the URL is "/securi ...

Tips for automatically resizing a canvas to fit the content within a scrollable container?

I have integrated PDF JS into my Vue3 project to overlay a <canvas id="draw_canvas"> on the rendered pdf document. This allows me to draw rectangles programmatically over the pdf, serving as markers for specific areas. The rendering proces ...

What's the reason behind the failure of bitwise xor within a JavaScript if statement?

I'm trying to understand the behavior of this code. Can anyone explain it? Link to Code function checkSignsWeird(a,b){ var output = ""; if(a^b < 0){ output = "The "+a+" and "+b+" have DIFFERENT signs."; }else{ output = ...

Communicating with Socket.io using the emit function in a separate Node.js module

I've been trying to make this work for the past day, but I could really use some assistance as I haven't had much luck figuring it out on my own. App Objective: My application is designed to take a user's username and password, initiate a m ...

creating grunt shortcuts with specified option values

Is it possible to create custom aliases in Grunt, similar to npm or bash? According to the Grunt documentation, you can define a sequence of tasks (even if it's just one). Instead of calling it "aliasing", I believe it should be referred to as "chaini ...

Switching to http2 with create-react-app: step-by-step guide

Can someone provide guidance on implementing http2 in the 'create-react-app' development environment? I've searched through the README and did a quick Google search but couldn't find any information. Your assistance is much appreciated. ...