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

Utilize Angular 2 Form Elements Again

I'm currently in the process of working on a project with Angular and I want to make sure that my form components can be used for creating and updating entities seamlessly. Let's say I have a User entity stored on a remote API, and I have a form ...

What is the best way to create an Office Script autofill feature that automatically fills to the last row in Excel?

Having trouble setting up an Excel script to autofill a column only down to the final row of data, without extending further. Each table I use this script on has a different number of rows, so hardcoding the row range is not helpful. Is there a way to make ...

Tips for finalizing a subscriber after a for loop finishes?

When you send a GET request to , you will receive the repositories owned by the user benawad. However, GitHub limits the number of repositories returned to 30. The user benawad currently has 246 repositories as of today (14/08/2021). In order to workarou ...

Tips for Validating Radio Buttons in Angular Reactive Forms and Displaying Error Messages upon Submission

Objective: Ensure that the radio buttons are mandatory. Challenge: The element mat-error and its content are being displayed immediately, even before the form is submitted. It should only show up when the user tries to submit the form. I attempted to use ...

Troubleshooting the inclusion of nodemon in package.json

I am interested in implementing nodemon to automatically recompile the project when there are changes made to the code during development. package.json { "name": "insurance-web-site", "version": "0.1.0", " ...

Encountering an error in testing with Typescript, Express, Mocha, and Chai

After successfully creating my first server using Express in TypeScript, I decided to test the routes in the app. import app from './Server' const server = app.listen(8080, '0.0.0.0', () => { console.log("Server is listening on ...

The assertion error `args[3]` must be an integer value, but it failed to meet the requirement

Software Version: v12.19.0 Operating System Platform: Linux ayungavis 5.4.0-48-generic #52~18.04.1-Ubuntu SMP Thu Sep 10 12:50:22 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux Subsystem: Steps to Reproduce the Issue I attempted to follow the tutorial provided ...

Hiding the line connector between data points in ChartJs

I recently took over a project that includes a line chart created using Chart.js by the previous developer. My client has requested that I do not display a line between the last two data points. Is this possible with Chart.js? I have looked through the doc ...

In a scenario where a specific element is disabled, how can I link its value to another related element?

Setting: Ionic version: 6.20.1 Angular CLI version: 10.0.8 In the process of developing a mobile expense management application, I am working on implementing a feature that calculates the recommended spending for different categories (e.g., home expense ...

experimenting with the checked attribute on a radio button with jasmine testing

Currently using Angular 8 with Jasmine testing. I have a simple loop of radio buttons and want to test a function that sets the checked attribute on the (x)th radio button within the loop based on this.startingCarType. I am encountering false and null tes ...

Ensure your TypeScript class includes functionality to throw an error if the constructor parameter is passed as undefined

My class has multiple parameters, and a simplified version is displayed below: class data { ID: string; desp: string; constructor(con_ID:string,con_desp:string){ this.ID = con_ID; this.desp = con_desp; } } When I retrieve ...

Creating reusable TypeScript function argument types

There is a function that I have defined in the following way: function getRangeBounds(start: number, stop?: number, step?: number) { if (step === undefined) step = 1; const actualStart = start !== undefined && stop !== undefined ? start : 0; ...

Utilizing ngFor to iterate over items within an Observable array serving as unique identifiers

Just starting out with Angular and I'm really impressed with its power so far. I'm using the angularfire2 library to fetch two separate lists from firebase (*.ts): this.list1= this.db.list("list1").valueChanges(); this.list2= this.db.list("list2 ...

Angular is not rendering styles correctly

Having two DOMs as depicted in the figures, I'm facing an issue where the circled <div class=panel-heading largeText"> in the first template receives a style of [_ngcontent-c1], while that same <div> gets the style of panel-primary > .p ...

Testing the React context value with React testing library- accessing the context value before the render() function is executed

In my code, there is a ModalProvider that contains an internal state managed by useState to control the visibility of the modal. I'm facing a dilemma as I prefer not to pass a value directly into the provider. While the functionality works as expecte ...

Function not functioning as expected in NestJS MongoDB unique field feature

I am trying to set the "unique:true" attribute for the name property in my NestJS - MongoDB schema, but it is not working as expected by default. @Schema() export class User { @Prop() userId:string; @Prop({ type:String, required:true, } ...

What is preventing type-graphql from automatically determining the string type of a class property?

I have a custom class named Foo with a property called bar that is of type string. class Foo { bar: string } When I use an Arg (from the library type-graphql) without explicitly specifying the type and set the argument type to string, everything works ...

What sets apart calling an async function from within another async function? Are there any distinctions between the two methods?

Consider a scenario where I have a generic function designed to perform an upsert operation in a realmjs database: export const doAddLocalObject = async <T>( name: string, data: T ) => { // The client must provide the id if (!data._id) thr ...

Unusual problem arises with scoping when employing typeguards

Consider the following TypeScript code snippet: interface A { bar: string; } const isA = <T>(obj: T): obj is T & A => { obj['bar'] = 'world'; return true; } let obj = { foo: 'hello' }; if (!isA(obj)) thro ...

Error message stating: "Form control with the name does not have a value accessor in Angular's reactive forms."

I have a specific input setup in the following way: <form [formGroup]="loginForm""> <ion-input [formControlName]="'email'"></ion-input> In my component, I've defined the form as: this.log ...