Typescript tip: Changing the property type in a discriminated union

I'm encountering a slight issue with TypeScript inference on discriminated unions. Let's say I have the following types:

type TypeA = {
  type: 'a';
  formData: number;
  onSubmit: (data: number) => void;
};

type TypeB = {
  type: 'b';
  formData: Date;
  onSubmit: (data: Date) => void;
};

type TypeAB = TypeA | TypeB;

For simplicity, let's assume I also have a function that takes a parameter typed as the discriminated union

export const MyForm = (props: TypeAB) => {
  const handleSubmit = () => {
    // do other things

    props.onSubmit(props.formData);
  };

  return  "return does no matter"
};

The issue arises when calling submit as TypeScript complains that props.formData may not match the expected type of onSubmit

https://i.sstatic.net/oxOsJ.png

However, based on the defined types, this should not be possible. onSubmit always corresponds to the type of formData

If I check for the type before calling onSubmit, it works without any issues

const handleSubmit = () => {
    // do other things
    if(props.type === 'a') props.onSubmit(props.formData);
    if(props.type === 'b') props.onSubmit(props.formData);

  };

Nevertheless, it seems unnecessary to perform this check. Any insights on why TypeScript is raising concerns here and how to properly define the discriminated union?

Answer №1

When analyzing code in TypeScript, it can only process a single block at a time. It would be beneficial if TypeScript could evaluate the line

props.onSubmit(props.formData);

twice, once assuming that props is of type TypeA, and another time assuming it is of type TypeB. However, this is not currently possible.

The analysis focuses on the types of props.onSubmit (a union type)

((data: number) => void) | ((data: Date) => void)
and props.formData (number | Date). The conclusion drawn is based solely on these types, disregarding the fact that both originate from the same props object. TypeScript does not account for identities, only types.

This results in the compiler losing the correlation between union types props.onSubmit and props.formData, as detailed in microsoft/TypeScript#30581.


The recommended solution, outlined in microsoft/TypeScript#47109, involves refactoring away from unions and instead utilizing generic indexed accesses into mapped types.

This approach allows the compiler to analyze the code block once with appropriately generic types, establishing correlations through the appearance of the generic type parameter.

To refactor the example provided, first create a simple mapping type from the old key type to the formData type using a utility interface:

interface TypeMap {
    a: number;
    b: Date;
}

Define TypeA, TypeB, and TypeAB within a distributive object type:

type TypeAB<K extends keyof TypeMap = keyof TypeMap> = { [P in K]: {
    type: P;
    formData: TypeMap[P];
    onSubmit: (data: TypeMap[P]) => void;
} }[K];

TypeAB represents TypeAB<"a" | "b">, equivalent to the original version. To obtain TypeA and TypeB, you can use the following:

type TypeA = TypeAB<"a">;

type TypeB = TypeAB<"b">;

Make MyForm generic by specifying the key type K:

export const MyForm = <K extends keyof TypeMap>(props: TypeAB<K>) => {
    const handleSubmit = () => {
        props.onSubmit(props.formData); // all good
    };   
    return "return does no matter"
};

This modification works because props.onSubmit is recognized as the function type

(data: TypeMap[K]) => void</code, and <code>props.formData
fits the type TypeMap[K], matching the parameter type for props.onSubmit.

For further insights into the refactoring process and its necessity, refer to microsoft/TypeScript#47109.

Playground link to 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

Angular's ng-select fails to select the value when generating the dynamic control

Currently, I am working on dynamically adding cities using ng-select in order to have search functionality while entering city names. For example, if I have two city names saved in a form and need to display them in separate ng-select controls when editing ...

Transform the date format from Google Forms to TypeScript

I am currently facing an issue with a Google Form connected to a Google Spreadsheet. The date format in the spreadsheet appears as follows when a response is received: 20/02/2023 18:58:59 I am seeking guidance on how to convert this date format using Type ...

Convert a fresh Date() to the format: E MMM dd yyyy HH:mm:ss 'GMT'z

The date and time on my website is currently being shown using "new date()". Currently, it appears as: Thu May 17 2018 18:52:26 GMT+0530 (India Standard Time) I would like it to be displayed as: Thu May 17 2018 18:43:42 GMTIST ...

The FormGroup Matrix in Angular 2+

Looking to develop an interface for creating a matrix, which requires two inputs for width and height. The matrix of inputs will vary based on these dimensions. I'm facing a challenge in making these inputs unique so they can be correctly associated ...

React Traffic Light Component: Colors Stuck After Timeout

I've been working on solving a React issue and followed a tutorial on YouTube. I'm using CodeSandbox for my project, but I'm facing a problem where the colors of the signal are not showing up and do not change after some time. I even tried u ...

Buttons for camera actions are superimposed on top of the preview of the capacitor camera

I am currently using the Capacitor CameraPreview Library to access the camera functions of the device. However, I have encountered a strange issue where the camera buttons overlap with the preview when exporting to an android device. This issue seems to on ...

An object that appears to be empty at first glance, but contains values that are undefined

I am facing an issue with my object that I am populating with information. The logs show the object as empty, but when I inspect it in Chrome, it appears to have the correct details filled in. Here is a snapshot of what the logs display: closed: closed o ...

What is the best way to create a mapping function in JavaScript/TypeScript that accepts multiple dynamic variables as parameters?

Explaining my current situation might be a bit challenging. Essentially, I'm utilizing AWS Dynamodb to execute queries and aiming to present them in a chart using NGX-Charts in Angular4. The data that needs to appear in the chart should follow this fo ...

The technique for concealing particular div elements is contingent upon the specific values within an array

My TypeScript code is returning an array in this format: allFlowerTypes (3) ['Rose', 'Bluebell' , 'Daisy'] I want to dynamically show or hide the following HTML content based on the array values above: <ul> <li> ...

Guide on integrating TypeScript with the Esri Leaflet JavaScript Plugin

I'm working on an Aurelia project in TypeScript that incorporates Leaflet for mapping. So far, I've been able to use typings for Leaflet itself, but the esri-leaflet plugin is only available in JavaScript. How can I import and utilize this JavaSc ...

How to use TypeScript to filter arrays with multiple dimensions

I've been attempting to filter an array with multiple filters, but I can't seem to achieve the desired outcome so far. This is my Angular component: list = [ {type: type1, code: code1}, {type: type2, code: code2}] searchElement(code?: string, ...

Implement a global interceptor at the module level in NestJS using the Axios HttpModule

Is there a way to globally add an interceptor for logging outgoing requests in Angular? I know I can add it per instance of HttpService like this: this.httpService.axiosRef.interceptors.request.use((config) => ...) But I'm looking to add it once a ...

Unable to utilize class identifiers in TypeScript because of 'incompatible call signatures' restriction

Following the execution of relevant yarn add commands, the following lines were added to the packages.json: "@types/classnames": "^2.2.7", "classnames": "^2.2.6", Subsequently, I incorporated these lines into my typescript files: import * as classnames ...

Using AngularJS to inject a service into a static property of a standard class

For my current project, I am combining TypeScript and AngularJS. One of the challenges I'm facing is how to instantiate a static member of a public class (not controller, just a normal class) with a service object. When it comes to controllers, utiliz ...

One effective way to transfer state to a child component using function-based React

My goal is to pass an uploaded file to a child component in React. Both the parent and child components are function-based and utilize TypeScript and Material-UI. In the Parent component: import React from 'react'; import Child from './ ...

Ionic application encountering 'push' property error in MQTT only when used within an if statement

Being new to Ionic and MQTT, I could really use some help with an issue I am facing. In my development environment of Ionic CLI PRO 4.3.1, I am attempting to navigate to a new page when a message is received from an MQTT topic. However, I am encountering a ...

Displaying a React component within a StencilJS component and connecting the slot to props.children

Is there a way to embed an existing React component into a StencilJS component without the need for multiple wrapper elements and manual element manipulation? I have managed to make it work by using ReactDom.render inside the StencilJS componentDidRender ...

Is it possible to utilize useRef to transfer a reference of an element to a child component?

When I want to mount something into the element with id tgmlviewer, I call it a viewer object. This process works smoothly when there is only one component of this kind. import React, { useEffect } from "react"; import { Viewer } from "../.. ...

What method can be used to verify if a username exists within Angular data?

We want to display online users on a webpage by checking if they are currently active. The current code logs all online users in the console, but we need to show this visually on the page. public isOnline: boolean = false; ... ... ngOnInit() { ...

Different ways to determine if a given string exists within an Object

I have an object called menu which is of the type IMenu. let menu: IMenu[] = [ {restaurant : "KFC", dish:[{name: "burger", price: "1$"}, {name: "french fries", price: "2$"}, {name: "hot dog", d ...