What is preventing me from using property null checking to narrow down types?

Why does TypeScript give an error when using property checking to narrow the type like this?

function test2(value:{a:number}|{b:number}){
    // `.a` underlined with: "Property a does not exist on type {b:number}"
    if(value.a != null){
        console.log('value of a:',value.a)
    }
}

test2({a:1})

test2({b:2})

The transpilation is successful, it functions correctly, and there are no runtime errors. The property check logically restricts the scope to {a:number}.

So what's the issue here? Is there a configuration I can modify in order to allow and use this method for type narrowing?

Someone might suggest using the in operator. However, I am aware that I could use the in operator, but I prefer not to. If I include a string in the union type as well, then I cannot use in with a string directly. I would need to first check that it's not a string with if(typeof value != 'string'). In regular JavaScript, I could simply rely on my property check and be confident that any value retrieved will have the property, ensuring everything works smoothly.

Therefore, why does TypeScript struggle with this scenario? Is it simply not intelligent enough to handle it?

(Changes: Originally, this question involved optional chaining to check for properties due to the type being unioned with null. To simplify, I've removed both the null and the optional chain operator from the if statement.)

Answer №1

What is the issue with TypeScript in this scenario?

It meticulously indicates that your code is attempting to access a property that has not been declared to exist on the object. If it were not a union type, you would naturally anticipate receiving this error message. Just because the code does not result in a runtime error does not mean that it is valid TypeScript. Reference link here.

If you are determined to access the property for discriminating the union, you must declare it on all type constituents - even if you specify it to have no value on the others:

function test2(value: { a: number; b?: never } | { b: number; a?: never }){
    if(value.a != null) {
        console.log('value of a:', value.a)
    }
}

test2({a:1})
test2({b:2})

Answer №2

If you're looking for a more detailed approach that avoids repeating properties for each type, consider using a type predicator. This function checks the object passed to it and determines if it fits the criteria for the desired type.

type A = {
  a: number;
};

type B = {
  b: number;
};

function isA(arg: unknown): arg is A {
  return !!arg && typeof arg === "object" && "a" in arg;
}

function isB(arg: unknown): arg is B {
  return !!arg && typeof arg === "object" && "b" in arg;
}

function test2(value: A | B) {
  if (isA(value)) {
    console.log("value of a:", value.a);
  }
  if (isB(value)) {
    console.log("Vale of b:", value.b);
  }
}

test2({ a: 1 });
test2({ b: 2 });

isA and isB are predicators that inform TypeScript about the type checks performed. While predicators are not foolproof and can be misleading by returning true when they shouldn't, they are useful for handling unique types with similar properties.

Check out this demo on Typescript playground: https://tsplay.dev/WJ0KZN. Also, read more about type predicates in the TypeScript documentation here: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

Answer №3

The discriminated union pattern could have been beneficial in this scenario. By including a field with a unique value in each interface, it becomes possible to differentiate the correct type when used in a union context.

For instance:

type NetworkLoadingState = {
  state: "loading";
};
type NetworkFailedState = {
  state: "failed";
  code: number;
};
type NetworkSuccessState = {
  state: "success";
  response: {
    title: string;
    duration: number;
    summary: string;
  };
};

type NetworkState =
  | NetworkLoadingState
  | NetworkFailedState
  | NetworkSuccessState;

function onNetworkStateChanged(network: NetworkState) {
  switch (network.state) {
    case "loading":
      console.log("Loading state...")
      break;
    case "failed":
      console.log("Network failed to load. Error code:", network.code);
      break;
    case "success":
      console.log(`Network is connected! Time taken: ${network.response.duration / 1000} seconds`);
      break;
  }
}

Playground link

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

Ways to apply the strategy pattern in Vue component implementation

Here's the scenario: I possess a Cat, Dog, and Horse, all of which abide by the Animal interface. Compact components exist for each one - DogComponent, CatComponent, and HorseComponent. Query: How can I develop an AnimalComponent that is capable of ...

How can I assign a type to an array object by utilizing both the 'Pick' and '&' keywords simultaneously in TypeScript?

As a beginner in TypeScript, I am looking to declare a type for an array object similar to the example below. const temp = [{ id: 0, // number follower_id: 0, // number followee_id: 0, // number created_at: '', // string delete_at: &ap ...

What could be triggering the "slice is not defined" error in this TypeScript and Vue 3 application?

I have been developing a Single Page Application using Vue 3, TypeScript, and the The Movie Database (TMDB) API. In my src\components\MovieDetails.vue file, I have implemented the following: <template> <div class="row"> ...

Is there a way to prevent the splash screen from appearing every time I navigate using a navbar link in a single page application (SPA)?

Recently, I came across this tutorial and followed it diligently. Everything seemed to be working perfectly until I encountered an issue with my navbar links. Each time I clicked on a link, the splash screen appeared, refreshing the page without directing ...

When transitioning from component to page, the HTTP request fails to execute

I have created a dashboard with a component called userInfo on the homepage. This component maps through all users and displays their information. Each user has a display button next to them, which leads to the userDisplay page where the selected user&apos ...

Encountering issue with FullCalendar and Angular 11: Error reading property '__k' of null

I am currently utilizing the Full Calendar plugin with Angular 11 but have encountered an error message stating "Cannot read property '__k' of null". It appears to be occurring when the calendar.render() function is called, and I'm strugglin ...

I am experiencing difficulties with TypeORM connecting to my Postgres database

Currently, I am utilizing Express, Postgres, and TypeORM for a small-scale website development project. However, I am encountering challenges when it comes to establishing a connection between TypeORM and my Postgres database. index.ts ( async ()=>{ ...

The argument '$0' provided for the pipe 'CurrencyPipe' is not valid

When retrieving data from the backend, I receive $0, but I need to display it as $0.00 in my user interface. <span [innerHTML]="session.balance | currency :'USD': true:'1.2-2'"></span> I'm encountering an issue where ...

The validation of DOM nesting has detected that a <td> element cannot be placed within an <a> element

When working on a React project with Material UI, I encountered an issue while trying to create a table. My goal was to make the entire row clickable, directing users to a page with additional information on the subject. Below is the snippet of code for th ...

How to customize Material UI Autocomplete options background color

Is there a way to change the background color of the dropdown options in my Material UI Autocomplete component? I've checked out some resources, but they only explain how to use the renderOption prop to modify the option text itself, resulting in a a ...

Angular: Defining variables using let and var

When working with TypeScript and JavaScript, we typically use either let or var to declare a variable. However, in Angular components, we do not use them even though Angular itself uses TypeScript. For instance, export class ProductComponent implements OnI ...

Create an interface that inherits from a class

I am currently in the process of converting some code that attempts to create an instance of http.ServerResponse. However, typescript is throwing an error: [ts] Property 'ServerResponse' does not exist on type 'typeof "http"'. I hav ...

Why do selected items in Ionic 3 ion-option not get deselected even after reloading or reinitializing the array

HTML File: <ion-item class="inputpsection" *ngIf="showDeptsec"> <ion-label floating class="fontsize12">Department</ion-label> <ion-select (ionChange)="showDepartmentChosen($event)" multiple="true" formControlName=" ...

Abort S3 file upload using ASW-SDK

Is there a way to abort an upload without raising an error Upload aborted. when calling upload.abort()? import { PutObjectCommandInput, S3Client } from '@aws-sdk/client-s3'; import { Progress, Upload } from "@aws-sdk/lib-storage"; cons ...

Ensure that a string contains only one instance of a specific substring

I need a function that removes all instances of a specific substring from a string, except for the first one. For example: function keepFirst(str, substr) { ... } keepFirst("This $ is some text $.", "$"); The expected result should be: This $ is some tex ...

An issue has arisen with the TypeScript function classes within the React Class, causing a compile error to be thrown

I am currently in the process of transitioning a React object from being a function to a class. This change is necessary in order to save the state and bind specific functions that I intend to pass to child components. Unfortunately, during compilation, I ...

Neglectful TypeScript null checks overlooking array.length verification

When TypeScript is compiled with strict null checks, the code snippet below does not pass type checking even though it appears to be correct: const arr: number[] = [1, 2, 3] const f = (n: number) => { } while (arr.length) { f(arr.pop()) } The comp ...

When passing e: EventTarget as a forwarded prop through a wrapper component, Typescript raises an error about the missing "value" property in the onChange function

In my project, there is a custom component called SelectField that serves as a wrapper for rendering label, helper text, and select input (inspired by TextField from @material-UI). The SelectField component exposes props like value and onChange, which are ...

MUI options - The specified type 'string' cannot be matched with type '"icon" | "iconOnly" | "text" | "outlined" | "contained" | undefined'

Is it possible to utilize custom variants in MUI v5? I am having trouble using a custom variant according to their documentation: https://mui.com/material-ui/customization/theme-components/#creating-new-component-variants declare module "@mui/material ...

Ways to trigger the keyup function on a textbox for each dynamically generated form in Angular8

When dynamically generating a form, I bind the calculateBCT function to a textbox like this: <input matInput type="text" (keyup)="calculateBCT($event)" formControlName="avgBCT">, and display the result in another textbox ...