Understanding the correct way to deduce the type T for an RxJS Subject<T> within a generic function that has a type constraint

I am currently working on a module that allows callers to subscribe to specific events. The caller provides the event name as the first argument when subscribing, and I want to be able to infer the callback signature from this argument.

In my implementation, I am using an RxJS Subject for each supported event and creating a subscription to it every time myModule.subscribe(eventType) is called. A simplified version of the implementation is shown below (you can also see it in action on import { Subject, Subscription } from "rxjs"; const EventNames = { a: "a", b: "b", c: "c", } as const; type Payloads = { [EventNames.a]: string; [EventNames.b]: number; [EventNames.c]: boolean; }; type EventTypes = keyof typeof EventNames; // all possible event objects type Events<T> = T extends EventTypes ? {type: T, payload: Payloads[T]} : never; // all possible callbacks type Callback<T> = T extends EventTypes ? (event: Events<T>) => void : never; // all possible subjects type Subjects<T> = T extends EventTypes ? Subject<Events<T>> : never; const collection = new Map<EventTypes, Subjects<EventTypes>>(); function fnWithConstraint<T extends EventTypes>( name: T, cb: Callback<T> ): Subscription | null { const subject = collection.has(name) ? collection.get(name) : new Subject<Events<T>>(); if (subject !== undefined) { collection.set(name, subject); /* ^ Type '{ type: "a"; payload: string; }' is not assignable to type 'Events<T>'. */ const subscription = subject.subscribe(cb) /* ^ This expression is not callable. Each member of the union type '{ (observer?: Partial<Observer<Events<T>>> | undefined): Subscription; (next: (value: Events<T>) => void): Subscription; ... 1 more ...' has signatures, but none of those signatures are compatible with each other. */ return subscription; } return null; } fnWithConstraint("b", (event) => console.log(event)); // expect typeof event -> { type: "b"; payload: number; }

I can't get this to compile successfully. The TS Playground I've linked to shows the correct result that I'm after on line:45, but the compiler is complaining about the Subjects type signature, and I can't seem to resolve it. What am I missing?

Answer №1

To achieve the desired functionality, you simply need to overload your function:

import { Subject, Subscription } from "rxjs";

const EventNames = {
  a: "a",
  b: "b",
  c: "c",
} as const;

type Payloads = {
  [EventNames.a]: string;
  [EventNames.b]: number;
  [EventNames.c]: boolean;
};

type EventTypes = keyof typeof EventNames;

// defining all possible event objects
type Events<T extends EventTypes> = { type: T, payload: Payloads[T] }

// specifying all potential callbacks
type Callback<T extends EventTypes> = (event: Events<T>) => void

// listing all valid subjects
type Subjects<T extends EventTypes> = Subject<Events<T>>

const collection = new Map<EventTypes, Subjects<EventTypes>>();

function fnWithConstraint<T extends EventTypes>(
  name: T,
  cb: Callback<T>
): Subscription | null
function fnWithConstraint(
  name: EventTypes,
  cb: Callback<EventTypes>
): Subscription | null {
  const subject = collection.has(name)
    ? collection.get(name)
    : new Subject<Events<EventTypes>>()

  if (subject !== undefined) {
    collection.set(name, subject);
    return subject.subscribe(cb)
  }
  return null

}

fnWithConstraint("b", (event) => console.log(event));

Playground

In this scenario, it is advisable to relax the strictness of the function for safety reasons.

The issue with your initial example arises from the usage of T which is a subtype. This does not imply that T is exactly equal to a | b | c; rather, it suggests that T could be any subtype within this union.

Consider the following modification:

function fnWithConstraint<T extends EventTypes,>(
  name: T,
  cb: Callback<T>
): Subscription | null {
  const subject = collection.has(name)
    ? collection.get(name)
    : new Subject<Events<T>>()

  if (subject !== undefined) {
    collection.set(name, subject);
    return subject.subscribe(cb)
  }
  return null
}

declare const a:'a' & {__tag:'Hello'}

fnWithConstraint(a, (event) => console.log(event));

The event argument now represents

Events<"a" & { __tag: 'Hello'; }>
.

While TypeScript allows the usage of const a, using the event argument in this context may be unsafe:

fnWithConstraint(a, (event) => console.log(event.type.__tag /** Hello */));

Remember that for TypeScript, a is a regular object capable of having its own subtypes. For instance, there can be more than 30 properties when using keyof 'a'.

You may apply branded types alongside my solution, yet the event argument will remain secure and valid.


Can class methods be overloaded in a similar manner?

Absolutely.

Here's a basic illustration:

class Foo {
  run(arg: string): number
  run(arg: number): string
  run() {
    return 'NOT IMPLEMENTED' as any

  }
}

const result = new Foo();
result.run(42) // string
result.run('str') // number

Playground

Official overloads documentation

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

Why isn't Nodemon monitoring the directory in webpack-typescript-node.js?

Here are the contents of the package.json file for a TypeScript project using webpack and node.js: "scripts": { "build": "webpack", "dev:start": "nodemon --watch src --exec \"node -r dotenv/co ...

how to adjust the width of a window in React components

When attempting to adjust a number based on the window width in React, I encountered an issue where the width is only being set according to the first IF statement. Could there be something wrong with my code? Take a look below: const hasWindow = typeof ...

Refresh the Angular view only when there are changes to the object's properties

I have a situation where I am fetching data every 15 seconds from my web API in my Angular application. This continuous polling is causing the Angular Material expansion panel to reset to its default position, resulting in a slow website performance and in ...

Determine the generic types of callback in TypeScript based on the argument provided

There are numerous Stack Overflow questions that share a similar title, but it seems none of them address this particular inquiry. I'm in the process of developing a wrapper for an express RequestHandler that can catch errors in asynchronous handlers ...

How to send a variable to Firestore query in an Angular application

I'm attempting to retrieve data from a schedule collection based on a field matching the user's id. However, I'm encountering an issue with the error message: "Function Query.where() requires a valid third argument, but it was undefined." ...

Angular 8 directive for concealing phone numbers

I am currently facing an issue with my phone mask directive that is designed to accept US format phone numbers. The problem arises when I try to clear the input by deleting one character at a time using the backspace key, as the value in the input field do ...

"Error: File is not recognized - Issue encountered while attempting to instantiate a File Object using types

I am working on a TypeScript program in Node.js, specifically a console application and not an API or web app. I have set up the TypeScript configuration, but I encountered an error when trying to create a File Object. UnhandledPromiseRejectionWarning: Ref ...

Will there be any impact on our modules and components if we update Angular 4 to the latest version of Angular?

I am working on a large project with a new team, and I would love to learn how to safely upgrade from Angular 4 to the latest version. I am relatively new to Angular, but I believe that the newest version is better than the old one! ...

What is the process for defining a generic function to convert to an array in TypeScript?

Here is a versatile function that wraps any value into an array: const ensureArray = <T,>(value?: T | T[]): T[] => { if (Array.isArray(value)) return value if (value === undefined) return [] return [value] } const undef = undefined ensureAr ...

Receiving error in TypeScript while using the 'required' attribute in the input field: "Cannot assign type 'string | undefined' to parameter expecting type 'string'"

In my TypeScript code, I am currently in the process of transitioning from utilizing useState to useRef for capturing text input values. This change is recommended when no additional manipulation necessitating state or rerenders is required. While I have ...

The system is unable to locate a supporting entity with the identifier '[object Object]', as it is classified as an 'object'

I'm currently working on an Angular 2 application where I am retrieving data from an API and receiving JSON in the following format. { "makes": null, "models": null, "trims": null, "years": null, "assetTypes": { "2": "Auto ...

What prevents TypeScript from automatically inferring tuple return types in RxJs streams?

When composing an observable stream, the map function infer is a union instead of a tuple. For instance: import { Component } from '@angular/core'; import { from } from 'rxjs'; import { map, tap } from 'rxjs/operators'; expo ...

What is the best way to manage a promise in Jest?

I am encountering an issue at this particular section of my code. The error message reads: Received promise resolved instead of rejected. I'm uncertain about how to address this problem, could someone provide assistance? it("should not create a m ...

What's the best way to convert a Union type into a Tuple with the same number of elements

My goal is to transform the following code snippet: type UnionType = Variant1 | Variant2 into the desired format below: type ResultingType = [UnionType, UnionType] In case the union comprises 3 member types, the tuple should contain 3 elements accordin ...

Can schemas be utilized within Deno development?

I have a data structure that I am looking to store and work with in Deno using MongoDB. Here is a similar example of the data format: [ { id: "AAPL", stockData: [ { date: 1634601600, summary: [Object] }, ...

What steps can be taken to eliminate the 404 error when refreshing an Angular 8 Single Page Application (SPA) without using

In my single page application project, I am utilizing Angular 8. Upon uploading my published code to the IIS server without using hash(#) in routing, I encounter a 404 error when attempting to refresh the page. Can anyone provide assistance on how to res ...

Discover the best practices for handling state in functional components and customizing styles with FlatList in React Native using TypeScript

Does anyone have a solution to these two issues? 1. When the list is pressed, I want to change the background color of the list from beige (#FFF5E7) to white (#FBFBFB). 2. Also, I need to update the read value of an Object from false to true using use ...

Guide to showcasing JSON Array in an HTML table with the help of *NgFor

Struggling to showcase the data stored in an array from an external JSON file on an HTML table. Able to view the data through console logs, but unable to display it visually. Still navigating my way through Angular 7 and might be overlooking something cruc ...

Oops! There was an unexpected error in the authGuard: [object Object] was not caught as expected

I've been working on implementing authGuard in my app, but I keep encountering an error. Below is the guard implementation: canActivate(route: ActivatedRouteSnapshot): Observable<boolean> { /** * Returning an observable of type boolea ...

Guidelines for creating a masterpage and details page layout in Angular 6

In my app.component.html file, I have the following code: <div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> </div> <div> <p-menu [model]="items"></p-menu> </div> Below is the code ...