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

Experiencing 429 Too Many Requests error on Angular 7 while attempting to perform multiple file uploads

Whenever I attempt to upload a large number of files simultaneously, I encounter an issue. The API interface only allows for the submission of one file at a time, requiring me to call the service for each individual file. Currently, my code looks like thi ...

Encountering a situation where the data retrieved from Firestore in Angular TypeScript is returning as

I am encountering an issue with retrieving the eventID value from my Events collection in Firestore. Although I am able to successfully display all the events in my app, I require the eventID for additional functionalities. As someone new to TypeScript an ...

Leverage server-side data processing capabilities in NuxtJS

I am currently in the process of creating a session cookie for my user. To do this, I send a request to my backend API with the hope of receiving a token in return. Once I have obtained this token, I need to store it in a cookie to establish the user' ...

How can a TypeScript function be used to retrieve a string (or JSON object)?

When attempting to retrieve data from a web API using TypeScript and return the JSON object, encountering an error has left me puzzled. Inside my function, I can successfully display the fetched data on the console, but when I try to return it with return ...

Why is it that the Jasmine test is unsuccessful even though the 'expected' and 'toBe' strings appear to be identical?

I have been developing a web application using Angular (version 2.4.0) and TypeScript. The application utilizes a custom currency pipe, which leverages Angular's built-in CurrencyPipe to format currency strings for both the 'en-CA' and &apos ...

Utilize the power of relative import by including the complete filename

When working on my TypeScript project, I have noticed that to import ./foo/index.ts, there are four different ways to do it: import Foo from './foo' // ❌ import Foo from './foo/index' // ❌ import Foo from './foo/i ...

Issue encountered: Unable to access the property 'loadChildren' as it is undefined, while attempting to configure the path

How can I conditionally load the route path? I've attempted the code below, but it's throwing an error. Can someone guide me on how to accomplish this task? [ng] ERROR in Cannot read property 'loadChildren' of undefined [ng] i 「w ...

Enhance your MaterialUI Button with a higher order component that accepts a component

I am currently working on implementing a Higher Order Component (HOC) for the MaterialUI Button component: import {Button as MUIButton, ButtonProps} from '@material-ui/core'; import * as React from 'react'; export const Button = (props ...

What could be the reason behind Typescript's unexpected behavior when handling the severity prop in Material UI Alerts?

Trying to integrate Typescript into my react project and encountering a particular error: Type 'string' is not assignable to type 'Color | undefined'. The issue arises when I have the following setup... const foo = {stuff:"succes ...

Understanding the differences between paths and parameters of routes in express.js

My express application has the following routes: // Get category by id innerRouter.get('/:id', categoriesController.getById) // Get all categories along with their subcategories innerRouter.get('/withSubcategories', categoriesControll ...

Creating an object efficiently by defining a pattern

As a newcomer to Typescript (and Javascript), I've been experimenting with classes. My goal is to create an object that can be filled with similar entries while maintaining type safety in a concise manner. Here is the code snippet I came up with: le ...

Having trouble compiling a Vue.js application with TypeScript project references?

I'm exploring the implementation of Typescript project references to develop a Vue application within a monorepo. The current structure of my projects is outlined below: client/ package.json tsconfig.json src/ ... server/ package.json t ...

Encountering an error message that says "ERROR TypeError: Cannot read property 'createComponent' of undefined" while trying to implement dynamic components in Angular 2

I am currently facing an issue with dynamically adding components in Angular 4. I have looked at other similar questions for a solution but haven't been able to find one yet. The specific error message I am getting is: ERROR TypeError: Cannot read ...

Utilizing TypeScript to reference keys from one interface within another interface

I have two different interfaces with optional keys, Obj1 and Obj2, each having unique values: interface Obj1 { a?: string b?: string c?: number } interface Obj2 { a: boolean b: string c: number } In my scenario, Obj1 is used as an argument ...

In TypeScript, both 'module' and 'define' are nowhere to be found

When I transpile my TypeScript using "-m umd" for a project that includes server, client, and shared code, I encounter an issue where the client-side code does not work in the browser. Strangely, no errors are displayed in the browser console, and breakpoi ...

What is the syntax for using typeof with anonymous types in TypeScript?

After reading an article, I'm still trying to grasp the concept of using typeof in TypeScript for real-world applications. I understand it's related to anonymous types, but could someone provide a practical example of how it can be used? Appreci ...

TypeScript will show an error message if it attempts to return a value and instead throws an

Here is the function in question: public getObject(obj:IObjectsCommonJSON): ObjectsCommon { const id = obj.id; this.objectCollector.forEach( object => { if(object.getID() === id){ return object; } }); throw new Erro ...

Tips for minimizing disagreements while implementing optional generic kind in TypeScript?

An issue arises in StateFunction due to its optional second generic type that defaults to a value. Even when omitting this second generic, undefined still needs to be passed as an argument, which contradicts the idea of it being optional. While making arg ...

Convert JSON data into an array of strings that are related to each other

I'm encountering an issue with converting JSON into a string array I must convert a JSON into a string array to dynamically INSERT it into our database, as I need this to work for any type of JSON without prior knowledge of its structure. Here is th ...

Trigger a dispatched action within an NGRX selector

I want to ensure that the data in the store is both loaded and matches the router parameters. Since the router serves as the "source of truth," I plan on sending an action to fetch the data if it hasn't been loaded yet. Is it acceptable to perform the ...