Narrowing the types of two variables

interface X {
   category: "X";
   descriptionOfX: string;
}

interface Y {
   category: "Y";
   detailsOfY: number;
}

interface Z {
   category: "Z";
   infoOfZ: boolean;
}

function compareObjects(x: X | Y | Z, y: X | Y | Z) {
  if (x.category !== y.category) {
    return false;
  }

  switch (x.category) {
    case "X": {
      return x.descriptionOfX === y.descriptionOfX;
    }
    case "Y": {
      return x.detailsOfY === y.detailsOfY;
    }
    case "Z": {
      return x.infoOfZ === y.infoOfZ;
    }
  }
}

Error occurs on all return statements within the switch case, as type y is not narrowed along with x (example shown for the first case):

Property 'descriptionOfX' does not exist on type 'X | Y | Z'.   
  Property 'descriptionOfX' does not exist on type 'Y'. [2339]

What is a concise method to handle this? The only solution I can think of is to include 9 type checks i.e. a switch/if statement for y in each case of x.

Answer №1

The Typescript compiler does not keep track of dependencies between different variables in a way that allows it to "remember" the equality of a.type and b.type. Despite being requested as a feature, the Typescript team, as evidenced by this issue, currently do not see it as a priority to implement.


If I were to refactor the code, I would simplify it using a switch statement like this:

  switch (a.type) {
    case "A": {
      return b.type === "A" && a.propsOfA === b.propsOfA;
    }
    case "B": {
      return b.type === "B" && a.propsOfB === b.propsOfB;
    }
    case "C": {
      return b.type === "C" && a.propsOfC === b.propsOfC;
    }
  }

By condensing the checks down to three from nine, the code becomes more concise. Another approach is to redefine your types as follows, creating a safer version that disallows certain invalid object structures:

interface A {
    type: "A";
    propsOfA: string;
    propsOfB?: never;
    propsOfC?: never;
}

interface B {
    type: "B";
    propsOfA?: never;
    propsOfB: number;
    propsOfC?: never;
}

interface C {
    type: "C";
    propsOfA?: never;
    propsOfB?: never;
    propsOfC: boolean;
}

With these refined types, your original code will compile without errors and provide additional safety checks. If manually writing out these types seems daunting, you can generate them programmatically using the following utilities:

/**
 * Creates a discriminated union type with enforced property requirements.
 */
type SafeUnion<T> = Simplify<T> extends infer _T ? _T extends unknown ? Replace<{[K in AnyKeyOf<T>]?: never}, _T> : never : never

/**
 * Simplifies a given type for improved readability.
 */
type Simplify<T> = T extends unknown ? {[K in keyof T]: T[K]} : never

/**
 * Combines two types similar to an intersection, but assigns shared properties according to the second type.
 */
type Replace<S, T> = Simplify<T & Omit<S, keyof T>>

/**
 * Retrieves all keys present in any member of a union type.
 */
type AnyKeyOf<T> = T extends unknown ? keyof T : never

After defining the above utilities, your code implementation can be updated to:

type ABC = SafeUnion<A | B | C>

function isEqual(a: ABC, b: ABC) {
    // ...
}

Playground Link

Answer №2

This code snippet is effective, straightforward, and minimally redundant:

function checkEquality(x: X | Y | Z, y: X | Y | Z) {
  if (x.type == "X" && y.type == "X") {
    return x.propertiesOfX === y.propertiesOfX;
  }
  if (x.type == "Y" && y.type == "Y") {
    return x.propertiesOfY === y.propertiesOfY;
  }
  if (x.type == "Z" && y.type == "Z") {
    return x.propertiesOfZ === y.propertiesOfZ;
  }
  return false;
}

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

Compatibility issues arise with static properties in three.d.ts when using the most recent version of TypeScript

When compiling three.d.ts (which can be found at this link) using the TypeScript develop branch, an error occurs with the following message: Types of static property 'Utils' of class 'THREE.Shape' and class 'THREE.Path' are i ...

Creating a Blob or ArrayBuffer in Ionic 2 and Cordova: Step-by-Step Guide

Is there a way to generate a blob or an arrayBuffer with TypeScript when using the Camera.getPicture(options) method? I am currently working on an Ionic 2/Cordova project. var options = { quality: 90, destinationType: Camera.DestinationType.FILE_ ...

Upon attempting to import the self-invoked jQuery library from query.selectareas.js in Angular2, an error occurred at runtime stating that jQuery

I'm facing a unique issue involving a self-invoked function that requires jquery. The specific library I need to use is: jquery.selectareas.js Here's the code snippet: import * as jQuery from 'jquery'; import "query.selectareas.js" ...

Issue: Pipe 'AsyncPipe' received an invalid argument '[object Object]'

I’m encountering an issue while attempting to replicate the steps from a specific YouTube tutorial. At the 8:22 mark of this video, I’m facing the following error: Error: InvalidPipeArgument: '[object Object]' for pipe 'AsyncPipe&apos ...

Inactive function

I have a function that inserts my articles and I call this function on my page. There are no errors, but the next function retrieveAllArticles() is not being executed. public saveAllArticles(article) { for(let data in article) { this.db.exec ...

TS2339 error occurs when the property 'takeUntil' is not found on the type 'Observable<Foo>' along with various other rxjs version 6 errors

After recently updating numerous packages in my Angular project, I encountered several compilation errors. Previous package.json: { "name": "data-jitsu", "version": "0.0.0", ... (old dependencies listed here) } New package.json: { "name": "da ...

How to convert typescript path aliases into relative paths for NPM deployment?

I am currently working on a typescript project that utilizes paths for imports. For instance: "paths": { "@example/*": ["./src/*"], } This allows the project to import files directly using statements like: import { foo } from "@example/boo/foo"; Whe ...

Utilize the application theme from Angular in your library module

I am currently working on an Angular library and I would like to incorporate the primary color of the application theme into my project. Unfortunately, I do not have access to the theme.scss file, so importing it into my component style file is not an opti ...

Configuring TypeORM to connect to multiple databases

For my backend project, I am utilizing node.js, TS, and typeorm as my tools. I have a requirement to establish a connection to a different database within the middleware based on the parameter I send, and then execute queries against that database. Here ...

Encountering two promises simultaneously within a try/catch block can lead to an "Unhandled promise rejection" error

Is there a way to run multiple promises in parallel without needing to await each one serially, which can slow down the process? I attempted to create two promises for separate network requests and then await them together to catch any errors in a try-cat ...

No data is generated when choosing from the dropdown menu in mat-select

I have a select function where all options are selected, but the selected sections are not shown. When I remove the all select function, everything works fine. Can you help me find the error? Check out my work on Stackblitz Here is my code: Select <m ...

Pause the execution at specific points within Typescript files by utilizing breakpoints when debugging with an attached debugger in VsCode

My current challenge involves setting up a debugger in VsCode with the attach mode on a Typescript codebase running in a Docker container. Despite successfully attaching the debugger in VsCode and hitting breakpoints, I consistently find myself landing on ...

Issue: MUI Autocomplete and react-hook-form failing to show the chosen option when using retrieved information

The MUI Autocomplete within a form built using react hook form is causing an issue. While filling out the form, everything works as expected. However, when trying to display the form with pre-fetched data, the Autocomplete only shows the selected option af ...

How can you determine if a slot in Stenciljs is unoccupied?

Is there a way to determine if a slot has a value or is empty? Below is the code I am using: <div> <slot name="info"></slot> </div> Here is the pseudo code of what I am trying to achieve: <div> <slot name ...

Angular - issue with getting window.screen.height in template

One issue I'm facing is when attempting to optimize a section of my template for mobile devices in landscape mode. TS: window = window; Template: <div [ngStyle]="{'height': window.innerHeight < window.innerWidth ? window.screen ...

Varieties of React components for arrays

Typically, when working with React functional components, we use React.FC<Props> as the return type. Take a look at this example component: const MyComponent = ({myArray}) => { return myArray.map( (item, index) => <span key={ind ...

Arranging an array of objects by their alphanumeric string property values

Currently, I am facing an issue with sorting an array of objects in TypeScript. The structure of my array is as follows: [ { "title": "Picture3.jpg", "targetRange": "B2", "type": ...

Encountering the issue "ReferenceError: Unable to reach 'Base' before initialization" while testing a Next.js web application on a local server

Below is the content of the complete index.tsx file: import { Base } from '../templates/Base'; const Index = () => <Base />; export default Index; I have researched other posts regarding this error message, but none resemble the struc ...

Ways to avoid Next.js from creating a singleton class/object multiple times

I developed a unique analytics tool that looks like this: class Analytics { data: Record<string, IData>; constructor() { this.data = {}; } setPaths(identifier: string) { if (!this.data[identifier]) this.da ...

I am interested in creating a class that will produce functions as its instances

Looking to create a TypeScript class with instances that act as functions? More specifically, each function in the class should return an HTMLelement. Here's an example of what I'm aiming for: function generateDiv() { const div = document.crea ...