Using Typescript to narrow down types for a workaround solution

When working with typescript, I am seeking a solution for a function that can take an argument of a specific type and return an object related to that type.

For instance, in the code snippet below, I am trying to narrow down the type of 'const response =' to be more specific than it currently is.

The scenario presented involves associating requests of certain types with responses that are only relevant to those particular requests. For example, when a request pertains to a user, the response should include their name and age. Conversely, if the request relates to a car, the response should contain details about the make and mileage of the vehicle. Each response should correspond exclusively to its associated request, such as 'user' for a user-related request and 'car' for a car-related request.

class RequestBase {
}

class ResponseBase {
}

interface IFindUserReq {
    user_id :string
}
class FindUserRequest implements IFindUserReq {
    user_id :string
    constructor(user_id) {
        this.user_id = user_id
    }
}

interface IFindUserRes {
    name :string
    age  :number
}
class FindUserResponse implements IFindUserRes {
    name :string
    age  :number
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

interface IFindCarReq {
    car_id :number
}
class FindCarRequest implements IFindCarReq {
    car_id :number 
    constructor(car_id) {
        this.car_id = car_id
    }
}

interface IFindCarRes {
    make :string
    miles :number
}
class FindCarResponse implements IFindCarRes {
    make :string
    miles  :number
    constructor(make, miles) {
        this.make = make;
        this.miles = miles;
    }
}

const request = new FindUserRequest("foo")
const response = performRequest(request) // Currently typed as 'RequestBase | undefined'. Is there a way to automatically narrow it down to FindCarResponse?

function performRequest(req :RequestBase) : RequestBase | undefined {
    if (req instanceof FindUserRequest) {
        return new FindUserResponse("foo", 23)
    } else if (req instanceof FindCarRequest) {
        return new FindCarResponse("toyota", 10000)
    }
}

UPDATE: Proposed Solution 1 Building upon Variable return types based on string literal type argument

One approach involves overloading the signature of 'performRequest' as shown below:

function performRequest(req :FindCarRequest) : FindCarResponse 
function performRequest(req :FindUserRequest) : FindUserResponse
function performRequest(req :RequestBase) : ResponseBase | undefined   {
    if (req instanceof FindUserRequest) {
        return new FindUserResponse("foo", 23)
    } else if (req instanceof FindCarRequest) {
        return new FindCarResponse("toyota", 10000)
    }
}

However, ideally, I would prefer not to modify the function signature of 'performRequest' in the application utilizing the request and response types from the library. Further suggestions are welcomed.

UPDATE: Alternative Solution 2 Credit to Gerrit Birkeland from TS Gitter channel for this innovative idea:

class RequestBase {
    _responseType : ResponseBase
}

class ResponseBase {
}

interface IFindUserReq {
    user_id :string
}
class FindUserRequest extends RequestBase implements IFindUserReq {
    _responseType :FindUserResponse
    user_id :string
    constructor(user_id) {
        super()
        this.user_id = user_id
    }
}

interface IFindUserRes {
    name :string
    age  :number
}
class FindUserResponse extends ResponseBase implements IFindUserRes {
    name :string
    age  :number
    constructor(name, age) {
        super()
        this.name = name;
        this.age = age;
    }
}

interface IFindCarReq {
    car_id :number
}
class FindCarRequest extends RequestBase implements IFindCarReq {
    _responseType :FindCarResponse
    car_id :number 
    constructor(car_id) {
        super()
        this.car_id = car_id
    }
}

interface IFindCarRes {
    make :string
    miles :number
}
class FindCarResponse extends ResponseBase implements IFindCarRes {
    make :string
    miles  :number
    constructor(make, miles) {
        super()
        this.make = make;
        this.miles = miles;
    }
}

const request = new FindUserRequest("foo")
const response = performRequest<FindUserRequest>(request) // The response type here is ResponseBase, an unexpected behavior

function performRequest< T extends RequestBase>(req :T) :T["_responseType"]    {

    if (req instanceof FindUserRequest) {
        return new FindUserResponse("foo", 23)
    } else if (req instanceof FindCarRequest) {
        return new FindCarResponse("toyota", 10000)
    } else {
        return new ResponseBase()
    }
}

Answer №1

To achieve the desired outcome, it is recommended to add a property to the RequestBase class. This particular property does not necessarily have to serve any functional purpose, but its presence is essential.

(Extracted from a gitter message)

class RequestBase {
  _responseType: ResponseBase
}

class ResponseBase {}

class FindUserRequest implements RequestBase {
  _responseType: FindUserResponse
  user_id: string
  constructor(user_id: string) {
    this.user_id = user_id
  }
}

class FindUserResponse {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const request = new FindUserRequest("foo")
const response = performRequest(request) // FindUserResponse | undefined

function performRequest<T extends RequestBase>(req: T): T["_responseType"] | undefined {
  if (req instanceof FindUserRequest) {
    return new FindUserResponse("foo", 23)
  }
}

Answer №2

To enable RequestBase to be generic, a generic type argument can be added to represent the response type. By doing this, it becomes possible to return the response type.

class RequestBase<TResponse> {
    performRequest() : TResponse {
        return null; // This is just a placeholder and should be replaced with actual request logic
    }
}
class FindCarRequest extends RequestBase<FindCarResponse> {
    constructor(public car_id: number) {
        super();
    }
}
class FindCarResponse {
    constructor(public make: string, public miles: number) {
    }
}

const request = new FindCarRequest(100)
const response = request.performRequest() // The returned type will be FindCarResponse

Note: It's not feasible to keep the performRequest function as an external function because type inference cannot determine the generic type parameter from the base type:

function performRequest<T>(req: RequestBase<T>):T {
    return null; // Placeholder only
}

const request = new FindCarRequest(100)
const response = performRequest(request) // Although expected to be FindCarResponse, the type will be {}

const request2 = new RequestBase<FindCarResponse>()
const response2 = performRequest(request) // The type will correctly be FindCarResponse

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

Zone Constraints for Dragging and Dropping in Angular 8

Seeking help to solve a puzzling issue that has been occupying my thoughts for the past few days. The Challenge I am attempting to incorporate a drag-and-drop functionality that allows users to drag up to 10 items and place them in specified spots (each ...

ESLint warning: Potentially risky assignment of an undetermined data type and hazardous invocation of an undetermined data type value

Review this test code: import { isHtmlLinkDescriptor } from '@remix-run/react/links' import invariant from 'tiny-invariant' import { links } from '~/root' it('should return a rel=stylesheet', () => { const resp ...

Ways to help a child notice when a parent's variable changes

Looking for help with passing data to a child component? Check out this Plunker: http://plnkr.co/edit/G1EgZ6kQh9rMk3MMtRwA?p=preview @Component({ selector: 'my-app', template: ` <input #x /> <br /> <child [value] ...

What steps are needed to generate an RSS feed from an Angular application?

I have a website built with Angular (version 12) using the Angular CLI, and I am looking to generate an RSS feed. Instead of serving HTML content, I want the application to output RSS XML for a specific route like /rss. While I plan on utilizing the rss p ...

Can someone please explain how to display a specific element from a JSON Array?

Is there a way to display only this specific part? /db/User_DataDb/61500546-4e63-42fd-9d54-b92d0f7b9be1 from the entirety of this Object obj.sel_an: [ { "__zone_symbol__state":true, "__zone_symbol__value":"/db/User_DataDb/61500546-4 ...

Sending data dynamically does not work in Angular forms

I am facing an issue with dynamically capturing text and sending it to my email. While manually typing into TS onSendEmail, it works fine. I am using a free email hosting server, and all the required files are available in the assets folder. HTML code < ...

Total the values of several items within the array

Here is the data I currently have: const arrayA = [{name:'a', amount: 10, serviceId: '23a', test:'SUCCESS'}, {name:'a', amount: 9, test:'FAIL'}, {name:'b', amount: ...

Error TS2322: The object with properties "ready: false" and "session: null" cannot be assigned to the type "Readonly<S & withAuthState>"

Here is the interface I'm currently working with: export interface withAuthState { ready: boolean, session: any } Additionally, I have developed the following Higher Order Component (HOC): const withAuth = <P extends withAuthProps, S extends ...

What steps should I take to resolve the error message "ESLint encountered an issue determining the plugin '@typescript-eslint' uniquely"?

Struggling to enable eslint linting in an ASP.NET Core MVC project that incorporates React.js and typescript? I'm facing a tough challenge trying to resolve the error mentioned above. In my setup, I'm using Visual Studio 2022 Community Edition 1 ...

Loading dynamic components asynchronously in Vue 3 allows for improved performance and enhanced user experience

My goal is to dynamically display components based on their type. Here's how I'm approaching it: I have several similar components that should show different content depending on their type. Using the defineAsyncComponent method, I can easily im ...

issues encountered when transforming a class into an interface

My instructor recommended that I turn the class into an interface, as it is the correct way to do it. export class Stats { donePomodoros: number; doneShortBreaks: number; doneLongBreaks: number; constructor() { this.donePomodoros = 0; this.done ...

Cannot execute example of type alias in Typescript

While delving into the Typescript documentation, I came across the concept of type alias and found an interesting example here. type DescribableFunction = { description: string; (someArg: number): boolean; }; function doSomething(fn: DescribableFunctio ...

Is there another option for addressing this issue - A function that does not declare a type of 'void' or 'any' must have a return value?

When using Observable to retrieve data from Http endpoints, I encountered an error message stating that "A function whose declared type is neither 'void' nor 'any' must return a value." The issue arises when I add a return statement in ...

The declaration file for the 'react' module could not be located

I was exploring Microsoft's guide on TypeScript combined with React and Redux. After executing the command: npm install -S redux react-redux @types/react-redux I encountered an error when running npm run start: Type error: Could not find a decla ...

The asyncData and fetch functions are failing to populate the data

I am currently working with nuxt v2.14.1 along with typescript and the nuxt-property-decorator package. I've been encountering a variety of issues. One of the problems I'm facing is the inability to set data from fetch() or asyncData. console. ...

What could be causing the Dynamic Options to fail to load on the material-react tailwind component?

I am facing an issue with a subcategory select menu that is supposed to display options based on the selected value in the category select input. Both inputs are utilizing the Select and Option components from the material-tailwind/react library. I have ex ...

Issues with setting up the .env file for Next.js project using OpenAI API and TypeScript

I'm currently exploring the capabilities of OpenAI's API. Initially, everything was running smoothly as I tested my API key directly within my code. However, upon attempting to move it to a .env file, I encountered some difficulties. Despite putt ...

Modify typescript prior to typechecking

Consider the following TypeScript file: class A { private x? = 0; private y? = 0; f() { console.log(this.x, this.y); delete this.x; } } const a = new A(); a.f(); When building it in webpack using awesome-typescript-loader ...

NuxtJS (Vue) loop displaying inaccurate information

I have a dataset that includes multiple languages and their corresponding pages. export const myData = [ { id: 1, lang: "it", items: [ { id: 1, title: "IT Page1", }, { ...

Angular 7 - Implementing periodic JSON data retrieval from server and maintaining local storage within Angular application

Seeking guidance on how to handle updating a static json file stored in the assets directory in an Angular 7 project. The goal is to periodically fetch a json from a server, check for updates, and perform post-processing on the data in the static file (ess ...