Determining the data type of a class member by analyzing a different member

Currently, I have a TypeScript class called Foo that consists of fieldOne: number | string and fieldTwo. Specifically, when fieldOne is a number, then fieldTwo will be of type Bar[]; whereas if fieldOne is a string, then fieldTwo becomes Baz[]. Both fields are passed as constructor parameters.

The majority of the class's functionality revolves around the union of Bar and Baz. However, there is a specific scenario where I need to narrow down the type to either Bar or Baz. Is there a method to utilize the TypeScript compiler to ensure that instances of Foo can only be created with either a number and Bar[], or a string and Baz[]? Furthermore, within the class, can I narrow down the type of fieldTwo to either Bar[] or Baz[] when necessary?

I was able to achieve this using inheritance, as demonstrated here.

// code snippets here 

Although effective, it feels somewhat cumbersome. I aimed for a more concise approach utilizing conditional types as shown here.

// code snippets here 

Unfortunately, this approach didn't align with my requirements as expected.

A third approach, showcased here, combines both fields into a single object.

// code snippets here 

However, even this method turned out to be lengthier than desired.

Answer №1

It's not possible to make this work with generics and conditional types in TypeScript because checking a generic value will not change the generic type itself. For that functionality, we would have to wait for something like microsoft/TypeScript#33014 to be implemented.


In cases where you need to check one property to narrow down the type of another property in TypeScript, you can do so when dealing with discriminated unions. However, if your properties are either strings or numbers, they won't serve as discriminants. To resolve this, you may introduce a separate literal type property, such as 'fieldZero', which could help achieve the desired narrowing:

type FooParams =
  { zero: true, one: number, two: Bar[] } |
  { zero: false, one: string, two: Baz[] };

class Foo {
  constructor(public params: FooParams) {}

  generalFunction() {
    console.log(this.params.two.map(x => x.foo).join());
  }

  specificFunction() {
    if (this.params.zero) {
      this.processBars(this.params.two);
    }
    else {
      this.processBazs(this.params.two);
    }
  }

  processBars(bars: Bar[]) {
    console.log(bars.map(x => x.bar).join());
  }

  processBazs(bazs: Baz[]) {
    console.log(bazs.map(x => x.baz).join());
  }
}

This approach makes 'FooParams' a discriminated union and eliminates the need for custom type guard functions.


If you prefer 'Foo' to actually be 'FooParams', not just contain it, things get more complicated. Creating union types from class statements directly in TypeScript is not supported. Despite this limitation, you can achieve a similar behavior by following these steps:

Firstly, rename the 'Foo' class to '_Foo' and define its base supertype:

class _Foo {

constructor(
 public fieldZero: boolean,
 public fieldOne: number | string,
 public fieldTwo: Bar[] | Baz[]
 ) { }
generalFunction() {
 console.log(this.fieldTwo[0].foo);
}
// Other methods }

Next, declare variants of 'Foo' using interfaces that extend '_Foo':

interface FooTrue extends _Foo {
 fieldZero: true,
 fieldOne: number,
 fieldTwo: Bar[]
}

interface FooFalse extends _Foo {
 fieldZero: false,
 fieldOne: string,
 fieldTwo: Baz[]
}

Now, define 'Foo' as the discriminated union type while maintaining a reference to '_Foo' as the underlying value with appropriate type assertions:

type Foo = FooTrue | FooFalse;
const Foo = _Foo as {
 new(fieldZero: true, fieldOne: number, fieldTwo: Bar[]): FooTrue,
 new(fieldZero: false, fieldOne: string, fieldTwo: Baz[]): FooFalse
}

With this setup, you can use 'Foo' like a discriminated union within '_Foo'. Ensure to specify the 'this' parameter within internal methods to enforce matching against 'Foo':

specificFunction(this: Foo) {
 if (this.fieldZero) {
 this.processBars(this.fieldTwo);
 }
 else {
 this.processBazs(this.fieldTwo);
 }
}

The above strategy avoids errors but may not be the most optimal solution given TypeScript's design focus on inheritance rather than alternation within a single class. Consider creating subclasses or utilizing union properties if the current implementation feels overly convoluted.

Access Playground link here

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

Exploring the Depths of Observables in Angular2 Guards

I have a Guardian overseeing a specific route. Within the canActivate method, I am trying to perform two HTTP requests, with the second request being dependent on the response of the first one. However, it seems like the second request is not being trigger ...

What is the process for configuring the Authorization Header in ng2-signalr?

I am currently utilizing the library ng2-signalr within my ionic 2 project. I am facing an issue regarding setting the authorization header, as I have been unable to find any examples on how to do so. Below is my code snippet for establishing a connection ...

Error: Astra connection details for Datastax could not be located

Currently, I am attempting to establish a connection to DataStax Astra-db using the cassandra-client node module. Below is an example of my code: const client = new cassandra.Client({ cloud: { secureConnectBundle: 'path/to/secure-connect-DATABASE_NA ...

TypeError: Unable to access the 'classify' property of an object that has not been defined (please save the ml5.js model first)

In my React app, I have set up ml5.js to train a model by clicking on one button and make predictions with another. However, I encounter an error when trying to test the model for the second time: TypeError: Cannot read property 'classify' of und ...

The drop down list does not support the 'click' function and is throwing a TypeError

Could someone please assist in troubleshooting my code? I am trying to select a specific value from a dropdown list but is encountering difficulties. The code is able to retrieve the values from the dropdown but is unable to successfully click on the mat ...

Displaying varying values of an object using TypeScript's console log trick

While using Angular version 8 with RJXS library, I encountered a peculiar issue when attempting to log an object variable: Object { carName: "Sorbonne", age: "20", type: "C1" } ​ carName: "BB" ​ age: "23&quo ...

The Lifespan of Angular Services as Perceived by the User

I'm struggling to grasp the concept of how services function from the user's point of view. Do services get initialized as singletons, covering the entire duration of the application or just the current user/session? For instance, if I save an ...

Type Vue does not contain the specified property

I am encountering an issue where I am using ref to retrieve a value, but I keep receiving the error message "Property 'value' does not exist on type 'Vue'". Below is the code snippet causing the problem: confirmPasswordRules: [ ...

What is the process for uploading an image with express-fileupload?

Looking to upload an image to Cloudinary via Postman using the express-fileupload library for handling multipart forms. Here is a snippet from my index.ts file: import fileUpload from "express-fileupload"; app.use(fileUpload()); In my controller ...

"Exploring the best practice: Defining types in React with Typescript before or after

As a newcomer to typescript, I have noticed that some projects declare the type before the component while others declare it after the component. Can someone explain the differences between these approaches? export type TProps = { item: string; } expor ...

Is there a more efficient method for invoking `emit` in Vue's Composition API from an external file?

Is there a more efficient way to access the emit function in a separate logic file? This is my current approach that is functioning well: foo.js export default (emit) => { const foo = () => { emit('bar') }; return { foo }; } When ...

What is the best way to incorporate data from a foreach method into a function call within an HTML string?

Having trouble calling a function with data from a foreach loop while generating HTML cards and buttons from an array. The issue seems to be in the renderProducts() method. /// <reference path="coin.ts" /> /// <reference path="prod ...

Collaborating with various sequential asynchronous services

In my component, I am injecting multiple services, two of which provide lists of objects needed by a third service. The problem is that the asynchronous calls to these services are not always executed in order. Nesting the calls inside each other does not ...

Find out whether the page was reloaded or accessed directly through a hyperlink

I need to find out if the page was accessed directly via a link. If it was, I need to perform a certain action. However, my current implementation is not working as intended, as even a page refresh triggers this action. Is there an alternative method to ch ...

Correcting Typing Errors in TypeScript

My current challenge involves a method that is consuming data from an external API: public async getStatus(id: string): Promise<APIRes | undefined> { try { const result = await getRequest(`${this.URL}/endpoint/${id}`) const respo ...

Vue + TypeScript prop type issue: "'Foo' is intended as a type, but is being treated as a value in this context."

As a newcomer to TypeScript and the Vue Composition API, I encountered an error that left me puzzled: I have a component that requires an api variable as a prop, which should be of type AxiosInstance: export default defineComponent({ props: { api: A ...

The specified property 'length' is not found on type OkPacket within the mysql2 module

In my code example below, I am simply showcasing a specific scenario: this.getCode = (code: string): Promise<codeObject | false> => { return new Promise((resolve, reject) => { pool.query('SELECT * FROM ?? WHERE code = ...

Having issues with TypeScript custom commands in Cypress?

Update: https://github.com/cypress-io/cypress/issues/1065#issuecomment-351769720 Removing an import from my commands.ts fixed it. Thanks In the process of transitioning my cypress project to use TypeScript, I am following the guidelines provided at https: ...

Is it possible to utilize an @Input() in Angular with multiple types?

Is it possible for a parent component to pass an object in @Input to the child component that may not always be the same? For instance, can I use: @Input() obj: string | number; In my scenario, I have two different objects as potential inputs: @Input() ob ...

What is the process for linking my component to my socket.io server?

I am facing a challenge in setting up a socket.io server to facilitate communication between two components: a command interface for sending data, and an overlay component for receiving it. Below is the code snippet: interface.component.html : <input ...