Utilize mapping for discriminated union type narrowing instead of switch statements

My objective is to utilize an object for rendering a React component based on a data type property. I am exploring ways to avoid utilizing switch cases when narrowing a discriminated union type in TypeScript. The typical method involves using if-else or switch statements, where TS automatically narrows the value:

type AView = {type: 'A', name: string} 
type BView = {type: 'B', count: number} 
type View = AView | BView

const prints =  {
    'A': (view: AView) => `this is A: ${view.name}`,
    'B': (view: BView) => `this is B. Count: ${view.count}`,
} as const

const outputWorks = (view: View): string => {
  switch (view.type) {
    case 'A':
    // or prints['A'](view)
      return prints[view.type](view)
    case 'B':
    // or prints['B'](view)
      return prints[view.type](view)
  }
}

outputWorks({type: 'A', name: 'John'})

I am curious whether there is a way to avoid using the switch statement and convince TS that the object can effectively narrow down the data:


type AView = {type: 'A', name: string} 
type BView = {type: 'B', count: number} 
type View = AView | BView

const prints =  {
    'A': (view: AView) => `this is A: ${view.name}`,
    'B': (view: BView) => `this is B. Count: ${view.count}`,
} as const

const outputFail = (view: View): string => prints[view.type](view)

outputFail({type: 'A', name: 'John'})

When implementing this approach, I encounter the error

The intersection 'AView & BView' was reduced to 'never' because property 'type' has conflicting types in some constituents.
due to TS failing to narrow down the type.

You can view and interact with this code on the TS Playground.

I have been working on resolving this issue for a week now and it seems quite challenging. Despite looking at various related discussions, I haven't been able to solve this problem yet.

Answer №1

The main issue at hand is the absence of direct support for correlated unions as outlined in microsoft/TypeScript#30581. Within the context of outputFail, the type of prints[view.type] is a union type denoted by

((view: AView) => string) | ((view: BView) => string)
, and the type of view is also a union type AView | BView. If all we know is that there exists a function f with the same type as prints[view.type], and a value v with the same type as view, then calling f(v) would not be considered safe. This uncertainty arises from the possibility that if v is an AView while f expects a (view: BView) => string, it could lead to errors.

However, in reality, this scenario is not valid. The union type of prints[view.type] and view are inherently linked in such a way that they always match. Unfortunately, the compiler lacks the capability to handle correlated unions.


The proposed solution to address this challenge can be found in microsoft/TypeScript#47109. The concept involves replacing unions with generics and restructuring the types to clearly convey to the compiler the compatibility between the function and its argument. In essence, f would have a generic type of (arg: Arg<K>) => void, while v would be of generic type Arg<K>.

Below is one possible implementation:

interface BaseView {
  A: { name: string };
  B: { count: number };
}

type View<K extends keyof BaseView = keyof BaseView> = {
  [P in K]: { type: P } & BaseView[P]
}[K];

In this redefinition, View is transformed into a generic distributive object type constructed from a basic mapping. Using no type argument with View gives the original version, but specifying the correct key allows retrieval of AView and BView types:

type AView = View<"A">;
type BView = View<"B">;


const printA = (view: AView) => {
  return `this is A: ${view.name}`
}

const printB = (view: BView) => {
  return `this is B. Count: ${view.count}`
}

To ensure proper typing when declaring prints, its type needs to be explicitly defined using a mapped type based on the same "base" mapping. This approach ensures the indexing results in a generic type rather than a union:

const prints: { [K in keyof BaseView]: (view: View<K>) => string } = {
  'A': printA,
  'B': printB,
}

Finally, here's how it all comes together:

const output = <K extends keyof BaseView>(view: View<K>): string => {
  const fn = prints[view.type];
  return fn(view); // everything aligns correctly
}

Upon closer inspection, fn has a type equivalent to

(view: View<K>) => string
, and view is of type View<K>, ensuring smooth execution.

Link to Playground with 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

What is the best way to handle missing values in a web application using React and TypeScript?

When setting a value in a login form on the web and no value is present yet, should I use null, undefined, or ""? What is the best approach? In Swift, it's much simpler as there is only the option of nil for a missing value. How do I handle ...

Assign the onClick function to the decoration of a Vscode extension

When I click on a vscode decoration, I want to trigger a function. Here's the code I created for this: const decoration = { range, hoverMessage: `${command} ${input}`, command: { title: 'Run Function', command: ' ...

What is the safest method to convert a nested data structure into an immutable one while ensuring type safety when utilizing Immutable library?

When it comes to dealing with immutable data structures, Immutable provides the handy fromJs function. However, I've been facing issues trying to integrate it smoothly with Typescript. Here's what I've got: type SubData = { field1: strin ...

What is the process for transforming a Typescript source file into JavaScript?

I have a basic HTML file with a script source set to index.ts. index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge ...

Combining the Partial<CssStyleDeclaration> union type with a dictionary can lead to potential typing complications when the implicit any flag is

Using VueJS v-bind:style binding makes it possible to set CSS variables. I am attempting to create a union type that allows for the object passed to v-bind:style to retain typings for CssStyleDeclaration, while also being relaxed enough to accept an arbitr ...

Is it possible for pdfjs-dist to be used with Typescript?

Is there a way to preview a PDF as a canvas without importing pdfjs-dist into my project? I have already used the command $yarn add pdfjs-dist to install pdfjs-dist. Do I need to include any additional imports? import pdfjsLib from "pdfjs-dist/build ...

Helping React and MUI components become mobile responsive - Seeking guidance to make it happen

My React component uses Material-UI (MUI) and I'm working on making it mobile responsive. Here's how it looks currently: But this is the look I want to achieve: Below is the code snippet for the component: import React from 'react'; i ...

How to Create a Flexible Angular Array Input Component

I have been working on developing reusable form input components using Angular's reactive forms. However, I am currently encountering challenges with my FormArray input component. To overcome some issues, I had to resort to using double-type casting ...

Guide to utilizing vue-twemoji-picker in TypeScript programming?

Has anyone encountered this issue when trying to use vue-twemoji-picker in a Vue + TypeScript project? I keep receiving the following error message. How can I fix this? 7:31 Could not find a declaration file for module '@kevinfaguiar/vue-twemoji-picke ...

Creating a dynamic union return type in Typescript based on input parameters

Here is a function that I've been working on: function findFirstValid(...values: any) { for (let value of values) { if (!!value) { return value; } } return undefined; } This function aims to retrieve the first ...

Revealing the Webhook URL to Users

After creating a connector app for Microsoft Teams using the yo teams command with Yeoman Generator, I encountered an issue. Upon examining the code in src\client\msteamsConnector\MsteamsConnectorConfig.tsx, I noticed that the webhook URL w ...

Can you explain the distinction between employing 'from' and 'of' in switchMap?

Here is my TypeScript code utilizing RxJS: function getParam(val:any):Observable<any> { return from(val).pipe(delay(1000)) } of(1,2,3,4).pipe( switchMap(val => getParam(val)) ).subscribe(val => console.log(val)); ...

Error: Vercel deployment of Next.Js app fails due to undefined localStorage

Encountering the issue ReferenceError: localStorage is not defined when attempting to deploy my Next.JS app on Vercel. const NewReserve: React.FC = () => { const setValue = (key: string, value: string) => { return localStorage.setItem(key, val ...

Issue with Material UI grid not rendering properly in TypeScript environment

I've been trying to replicate a grid from material-ui using React and Typescript. You can see a live demo here. I modified the example to work with Typescript, so my demo.tsx file looks like this: Code goes here... If you check out the live demo, y ...

Transforming Post Requests into Options Requests

I am facing an issue with my Angular 6 API. After creating interceptors, my POST requests are turning into OPTIONS requests. What could be causing this problem? Here is the code for one of the Services: import { Injectable } from '@angular/core&apo ...

Creating Child Components in Vue Using Typescript

After using Vue for some time, I decided to transition to implementing Typescript. However, I've encountered an issue where accessing the child's methods through the parent's refs is causing problems. Parent Code: <template> <re ...

Incorporate Typescript into specific sections of the application

I've got a Node.js application that's entirely written in JavaScript. However, there are certain sections of my app where I'd like to utilize TypeScript interfaces or enums. Is there a way for me to incorporate Typescript into only those s ...

Placing a MongoDB query results in an increase of roughly 120MB in the total JS heap size

I'm puzzled by the fact that the heap size increases when I include a MongoDB database query in a function within my controller. Here is the code for my router: import { Router } from "express"; import profileController from '../contro ...

Error 2300 in Vetur: Identical identifier found for '(Missing)'

Recently, I've been encountering a strange issue with Vetur in my typescript nuxt.js project. Every component, whether it's just an empty line or contains code, displays an error message on the first line. I can't pinpoint when this problem ...

Angular 6 component experiencing issues with animation functionality

I've implemented a Notification feature using a Notification component that displays notifications at the top of the screen. The goal is to make these notifications fade in and out smoothly. In my NotificationService, there's an array that holds ...