Tips for preventing a wrapped union type from collapsing

It seems like there is an issue with Typescript collapsing a wrapped union type when it knows the initial value, which is not what I want. I'm uncertain if this is a bug or intended behavior, so I'm curious if there's a way to work around it.

Here's an example:

type Prime = 3|5|7;
type OrDonuts<T> = T | 'donuts';

function compare<T>(a: T, b: OrDonuts<T>) {
    return a == b;
}

let value: Prime;
compare(value, 3);
// Passes without errors

value = 5;
compare(value, 3);
// Error: Argument of type '5' is not assignable to parameter of type 'OrDonuts<3>'

To bypass this error, I have to explicitly uncollapse by doing something like value = 5 as Prime;.

Is this unexpected behavior, a bug, or am I missing something?

(node: 10.15, typescript: 3.5.1)

Answer №1

The TypeScript type checker utilizes control flow type analysis, which means it tries to determine how values inside variables change at runtime and adjusts the types of these variables accordingly. When dealing with union types, if a more specifically-typed value is assigned to a variable or property with that union type, the compiler will narrow the variable's type to be that specific type until another value is assigned.

This feature is often advantageous in scenarios like this:

let x: number | string = Math.random() < 0.5 ? "hello" : "goodbye"; // x is now a string
if (x.length < 6) { // no error here
  x = 0; // since x can only be a string or a number, this assignment is valid
}

Even though x is annotated as a number | string, the compiler understands that after the initial assignment it will definitively be a

string</code. Therefore, it permits operations like <code>x.length
without complaints, as it knows x cannot potentially be a
number</code. This behavior is so helpful that disabling it would cause issues for real-world TypeScript code.</p>

<p>However, this feature is also responsible for the issue you are encountering. After assigning <code>5
to value, the compiler perceives value as holding a value of narrowed type 5 instead of Prime. The compiler alerts you about calling
compare(5 as 5, 3)</code, thinking it's incorrect. To work around this, you must manually widen <code>value
to
Prime</code through type assertion.</p>

<p>To address this, you have different options available such as using type assertion during the initial assignment or within the call to <code>compare()
. You can also specify the generic type T in your compare() call to resolve the issue.

The most comprehensive source of information on this topic can be found in Microsoft/TypeScript#8513, particularly in this comment.

Hopefully, this explanation helps you navigate through the situation successfully. Good luck!

Link to code

Answer №2

Union Types behave as expected. When you define:

type Prime = 3|5|7; // it is either 3 OR 5 OR 7 but never all of them at the the same time

You are specifying that Prime can only be one of those values. If you assign 5 to a variable of type Prime, like value, for example, then Prime becomes 5.

value = 5; 
compare(value, 3); // The ts-compiler considers 'T' to be 5

To address this issue, ensure you use value:Prime or perform type assertion as you did initially.


If there was no type inference, you could mistakenly pass value like so:

value = 5;
compare<2>(value, 3); // Argument of type '5' is not assignable to parameter of type '2'.

Alternatively, if you supplied a value that aligns with the compare function's generic parameter:

let test = 5;
compare(test, 3); // 'T' now refers to a number since both 5 and 3 fit in that category.

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

Working with Typescript to map and sort the key values of a new datasource object

Managing a large datasource filled with objects can be challenging. My goal is to rearrange the order of objects in the array based on new values for each key. Whenever a new value for a key is found, I want the corresponding object to move to the top of t ...

The Node.js express seems to be unable to fetch the css and js files

Sharing my main.ts file in which I am facing issues with linking my css and js files. view image import express from 'express'; import {Request,Response} from 'express'; import expressSession from 'express-session'; import pat ...

Tips for retrieving Angular routing data from external sources outside of an Angular application

Is there a way to automatically generate a sitemap for each module during build time? The project structure is as follows: - cli - client -- Module A -- Routing A -- Module B -- Routing B -- Module C -- Routing C - server I am ...

"Experience the power of utilizing TypeScript with the seamless compatibility provided by the

I'm attempting to utilize jsymal.safeDump(somedata). So far, I've executed npm install --save-dev @types/js-yaml I've also configured my tsconfig file as: { "compilerOptions": { "types": [ "cypress" ...

Is there a way for me to connect to my Firebase Realtime Database using my Firebase Cloud Function?

My current challenge involves retrieving the list of users in my database when a specific field is updated. I aim to modify the scores of players based on the structure outlined below: The Realtime Database Schema: { "users": { &quo ...

Declaration files for Typescript ESLint configurations

I've been researching this issue online, but I haven't been able to find any solutions. It could be because I'm not entirely sure what's causing the problem. What I'm trying to do is set a global value on the Node.js global object ...

What is the reason that property spreading is effective within Grid components but not in FormControl components?

Explore the sandbox environment here: https://codesandbox.io/s/agitated-ardinghelli-fnoj15?file=/src/temp4.tsx:0-1206. import { FormControl, FormControlProps, Grid, GridProps } from "@mui/material"; interface ICustomControlProps { gridProps?: ...

How can you store form field validation rules (for example, firstname.dirty) in an array within TypeScript in Angular?

How do I save form field validation rules in an array? What should replace /'''''HERE'''''/ with? formfields: Array<Object> = [ {label: "Employer:", control: "employer", val ...

Troubleshooting Issue with Chrome/chromium/Selenium Integration

Encountered an issue while attempting to build and start the application using "yarn start": ERROR:process_singleton_win.cc(465) Lock file cannot be created! Error code: 3 Discovered this error while working on a cloned electron project on a Windows x64 m ...

Comparable to LINQ SingleOrDefault()

I frequently utilize this particular pattern in my Typescript coding: class Vegetable { constructor(public id: number, public name: string) { } } var vegetableArray = new Array<Vegetable>(); vegetableArray.push(new Vegetable(1, "Carrot")); ...

I am experiencing issues with my Jest unit test case for Material UI's multi select component failing

I've been working on writing a unit test case for the material UI multi select component. The code for the parent component is as follows: import {myData} from '../constant'; export const Parent = () => { const onChangeStatus= (sel ...

Failed deployment of a Node.js and Express app with TypeScript on Vercel due to errors

I'm having trouble deploying a Nodejs, Express.js with Typescript app on Vercel. Every time I try, I get an error message saying "404: NOT_FOUND". My index.ts file is located inside my src folder. Can anyone guide me on the correct way to deploy this? ...

Error in Typescript: An element is implicitly assigned the 'any' type because a string expression is being used to index a different type

Hello everyone, I'm fairly new to TypeScript and I've been struggling to troubleshoot an error in my code. Can someone please assist me with solving this TypeScript error? I keep getting the error message: "Element implicitly has an 'any&a ...

Guide on verifying if a variable is a tuple in TypeScript

I am attempting to determine if a variable passed to a function, which can either be an array of numbers or an array of tuples, is the array of tuples. function (times: Array<number> | Array<[number, number]>) { if (Array.isArray(times[0]) ...

Separate configurations for Webpack (Client and Server) to run an Express app without serving HTML files

I am currently developing an application with a Node Backend and I am trying to bundle it with Webpack. Initially, I had a single Webpack configuration with target: node. However, I encountered issues compiling Websockets into the frontend bundle unless I ...

What are the steps for customizing the interface in TypeScript?

After fixing a type error related to adding custom functions to the gun chain by including bind():any within IGunChainReference in @types/gun/index.ts, I am wondering how to transfer this modification to one of my project files. I have not been able to fi ...

What could be causing the Angular router outlet to not route properly?

Check out this demo showcasing 2 outlets (Defined in app.module.ts): <router-outlet></router-outlet> <router-outlet name="b"></router-outlet> The specified routes are: const routes: Routes = [ { path: 'a', com ...

Definition for TypeScript for an individual JavaScript document

I am currently attempting to integrate an Ionic2 app within a Movilizer HTML5 view. To facilitate communication between the Movilizer container client and the Ionic2 app, it is crucial to incorporate a plugins/Movilizer.js file. The functionality of this f ...

Incorporate axios within getStaticProps while utilizing Next.js

I am working on a new project where I am utilizing axios to handle request data. However, I am facing an issue when using axios in the getStaticProps function which results in an error on my index.js page. Here is a snippet of my index.js code: import ...

How to Retrieve Input Field Value Using Cypress Custom Command

Is there a way to retrieve the value of an input[text] element within a custom command? Cypress.Commands.add('extendValue', { prevSubject: 'element' }, (subject: JQuery<HTMLElement>, extension: string): any => { const r ...