Restricting union types by a specific property

I'm facing an issue when attempting to narrow down a type based on a property. To explain it better, here's a simplified version in code:

type User = {
  id: number;
  name: string;
}

type CreateUser = {
  name?: string;
}

const user: User | CreateUser = { name: "Foo Bar" };

if (user.id) {
  console.log(`Hello ${user.name}`)
}

My thought process is that user could be either a User or a CreateUser, and we can differentiate based on the presence of the id property. However, this approach fails with the error message:

Property 'id' does not exist on type 'CreateUser'
. Additionally, user.name remains as string | undefined instead of just being a string. Is there a way to achieve this that I am missing?

Surprisingly, even this approach doesn't work:

if ("id" in user) {
  console.log(`Hello ${user.name}`)
}

This results in

Property 'name' does not exist on type 'never'.
.

Answer №1

When you specify a type annotation and initialize a value, the compiler is intelligent enough to narrow down the value to the type you use for initialization. For instance, if you initialize it with a type that only has the name property and lacks the id property, the compiler will narrow it to the CreateUser type:

const user: User | CreateUser = { name: 'Foo Bar' };

user;
//^? const user: CreateUser

If you want to prevent this automatic narrowing and maintain the union information in the same scope, you can use an assertion instead of an annotation:

const user = { name: 'Foo Bar' } as User | CreateUser;

if ('id' in user) {
          //^? const user: User | CreateUser
  console.log(`Hello ${user.name}`);
                     //^? const user: User
}

TS Playground


Replying to your comment:

This approach is also valid when exporting a value. The key is that whenever you assign a value to the variable and wish to retain the entire union type in that scope, you must utilize an assertion to overwrite the compiler's automatic narrowing behavior:

TS Playground

export let user: User | CreateUser;

user = { name: 'Foo Bar' } as User | CreateUser;

if ('id' in user) {
          //^? const user: User | CreateUser
  console.log(`Hello ${user.name}`);
                     //^? const user: User
}

If this process becomes cumbersome, you can create an alias for the union type:

export type AnyUser = User | CreateUser;

export let user: AnyUser;

user = { name: 'Foo Bar' } as AnyUser;

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

Add a service to populate the values in the environment.ts configuration file

My angular service is called clientAppSettings.service.ts. It retrieves configuration values from a json file on the backend named appsettings.json. I need to inject this angular service in order to populate the values in the environment.ts file. Specific ...

Refreshing a page in Angular 4/5 using TypeScript

I am currently working on a single-page application that utilizes routes for navigation: this.router.navigate(['/customer-home'], { skipLocationChange: true }); While on the customer home page, I have a feature to add a new customer via a REST ...

Unlock the Power of Typography in React with Material UI Styled Components

Exploring the world of material UI is a new experience for me. I am currently in the process of creating a styled component using Typography. Below is what I have attempted so far: import styled from 'styled-components'; import { FormGroup, ...

What is the best way to create an instance method in a subclass that can also call a different instance method?

In our programming project, we have a hierarchy of classes where some classes inherit from a base class. Our goal is to create an instance method that is strongly-typed in such a way that it only accepts the name of another instance method as input. We d ...

Is it possible to define TypeScript interfaces in a separate file and utilize them without the need for importing?

Currently, I find myself either declaring interfaces directly where I use them or importing them like import {ISomeInterface} from './somePlace'. Is there a way to centralize interface declarations in files like something.interface.ts and use the ...

The CSS scale property is not working as expected when used in a React.js application, specifically

working environment ・next.js ・react ・typescript https://www.youtube.com/watch?v=ujlpzTyJp-M A Toolchip was developed based on the referenced video. However, the --scale: 1; property is not being applied. import React, { FunctionComponent ...

Attaching dynamic data to a specific element within an array

I have successfully created a demo where elements can be dropped into a specific area and their top and left values are displayed. I have also added functionality to remove dropped items and move them between different blocks. However, I am encountering so ...

Issue with handsontable numbro library occurs exclusively in the production build

Encountering an error while attempting to add a row to my handsontable instance: core.js.pre-build-optimizer.js:15724 ERROR RangeError: toFixed() digits argument must be between 0 and 100 at Number.toFixed () at h (numbro.min.js.pre-build-op ...

Angular2 encountering a lack of service provider issue

After finding the code snippet from a question on Stack Overflow titled Angular2 access global service without including it in every constructor, I have made some modifications to it: @Injectable() export class ApiService { constructor(public http: Http ...

Fast + Vue3 + VueRouter Lazy Load Routes According to Files

I am currently working on implementing Domain-Driven Design (DDD) to Vue, and the project structure looks like this: src - App - ... - router - index.ts - Dashboard - ... - router - index.ts - ... The goal is for src/App/router/index.ts to ...

"Utilizing Firebase Functions to update information in the Firebase Realtime Database on a daily basis

Currently, I am in the process of working on a project where I aim to provide users with a daily percentage of points based on their current available points and update this data in my Firebase database. My goal is to add points for users on a day-to-day b ...

Transforming named functions in const or class constructors with Eslint and Typescript: a guide

Lately, I've been relying heavily on the code snippet from an answer that I requested: function sameValuesAsKeys<K extends string>(...values: K[]): {readonly [P in K]: P} { const ret = {} as {[P in K]: P} values.forEach(k => ret[k] = k); ...

Adjusting canvas height in Storybook - Component does not fit properly due to low canvas height

I had a component that I needed to add to Storybook. It was working fine, but the styling was slightly off. I managed to resolve this by adding inline styling with position: absolute. Here is how it looks now: const Template: any = (args: any): any => ( ...

Tips for accessing and adjusting an ngModel that is populated by an attribute assigned via ngFor

Looking for guidance on how to modify an input with ngModel attribute derived from ngFor, and update its value in the component. Here is my code snippet for reference: HTML FRONT The goal here is to adjust [(ngModel)] = "item.days" based on button click ...

Using string interpolation and fetching a random value from an enum: a comprehensive guide

My task is to create random offers with different attributes, one of which is the status of the offer as an enum. The status can be “NEW”, “FOR_SALE”, “SOLD”, “PAID”, “DELIVERED”, “CLOSED”, “EXPIRED”, or “WITHDRAWN”. I need ...

I am experiencing issues with my Angular2 project where not all of my component variables are being passed to the

My problem involves sending three variables to my HTML template, but it only recognizes one of them. The HTML template I am using looks like this: <div class="page-data"> <form method="post" action="api/roles/edit/{{role?.ID}}" name="{{role? ...

Connecting Multiple Relationships with Many-To-Many Matches

My database consists of the following entities: @Entity class User { @ManyToMany(type => Group) @JoinTable() groups: Group[]; } @Entity class MediaObject { @ManyToMany(type => Group) @JoinTable() groups: Group[]; } @Entity ...

Is there a way to access every item that includes a particular attribute and attribute term within the woocommerce-rest-api?

Struggling to retrieve products that have the "x-attribute" and "x-attribute-term" using Node.js with the WooCommerce REST API library from here. I've explored solutions on stackoverflow and other sites but none seem to work. Atte ...

Using TypeScript to destructure by providing types

I encountered an issue while trying to destructure some code. The error message Property 'name' does not exist on type '{}'. is appearing. I thought about using let user:any = {}; as a workaround, but that goes against the eslint rule o ...

The parameter type SetStateAction<MemberEntityVM[]> cannot be assigned the argument type Promise<MemberEntityVM[]> in this context

I am looking to display a filtered list of GitHub members based on their organization (e.g., Microsoft employees). Implementing React + TS for this purpose, I have defined an API Model which represents the structure of the JSON data from the GitHub API: ex ...