What can we achieve using typename and nested types in a Typescript generic function?

I've been exposed to numerous tricks, but I seem to be struggling with this particular puzzle; therefore, any assistance from someone with more experience in TS would be greatly appreciated.

My subscribe() function requires:

  1. Message type in the form of a string
  2. The generic type parameters within the given Message type

In simpler terms, this is what I aim to achieve:

messaging.subscribe<NameAgeMessage>(
  (message) => {
    console.log(`received message type '${message.messageType}', with id=${message.msg.id}`);
  });

However, I'm currently dealing with:

messaging.subscribe<NameAgeMsg, NameAgeSatisfyArg>(
  NameAgeMessage.MESSAGE_TYPE,
  (message) => {
    console.log(`received message type '${message.messageType}', with id=${message.msg.id}`);
  });

This is due to my struggle with understanding method signatures and type nuances:

public subscribe<TMsg, TSatisfyArg>(
  messageType: string,
  observer: (message: AppMessage<TMsg, TSatisfyArg>) => void): Subscription {

  const messagesFiltered = this.messages
    .pipe(filter((message) => message.messageType === messageType));

  return messagesFiltered.subscribe(observer as any);
}

If you wish to explore alternative syntax, check out the lower section of this code scaffold:

(Link to Playground for Code Execution)

import { BehaviorSubject, Subscription, filter } from "rxjs";

abstract class AppMessage<TMsg, TSatisfyArg> {
  abstract readonly messageType: string;
  abstract readonly msg: TMsg;
  satisfy!: undefined | ((msg: TMsg, satisfyArg: TSatisfyArg) => void);

  protected constructor() { }
};

class MessagingServiceStartupMessage extends AppMessage<undefined, undefined> {
  public static readonly MESSAGE_TYPE: string = 'message-service-startup-message';
  public readonly messageType = MessagingServiceStartupMessage.MESSAGE_TYPE;

  constructor(
    public readonly msg: undefined
  ) {
    super();
  }
}

type NameAgeMsg = { id: number };
type NameAgeSatisfyArg = { name: string, age: number };

class NameAgeMessage extends AppMessage<NameAgeMsg, NameAgeSatisfyArg> {
  public static readonly MESSAGE_TYPE: string = 'name-age-message';
  public readonly messageType = NameAgeMessage.MESSAGE_TYPE;
  constructor(
    public readonly msg: NameAgeMsg
  ) {
    super();
  }
}

class MessagingService {
  private messages: BehaviorSubject<AppMessage<any, any>> = new BehaviorSubject<AppMessage<any, any>>(
    new MessagingServiceStartupMessage(undefined));

  public publishForSatisfy<TMsg, TSatisfyArg>(
    message: AppMessage<TMsg, TSatisfyArg>,
    satisfy: (msg: TMsg, satisfyArg: TSatisfyArg) => void
  ): void {

    message.satisfy = satisfy;
  }

  // Needs:
  //   1. Message type as string
  //   2. The generic type parameters in the provided Message type

  public subscribe<TMsg, TSatisfyArg>(
    messageType: string,
    observer: (message: AppMessage<TMsg, TSatisfyArg>) => void): Subscription {

    const messagesFiltered = this.messages
      .pipe(filter((message) => message.messageType === messageType));

    return messagesFiltered.subscribe(observer as any);
  }
}

class TestClass {
  messaging: MessagingService = new MessagingService();

  testSubscribe(): void {

    // QUESTION: Can this be cleaned up...
    this.messaging.subscribe<NameAgeMsg, NameAgeSatisfyArg>(
      NameAgeMessage.MESSAGE_TYPE,
      (message) => {
        console.log(`received message type '${message.messageType}', with id=${message.msg.id}`);
      });

    // ...to be something like this?
    this.messaging.subscribe<NameAgeMessage>(
      (message) => {
        console.log(`received message type '${message.messageType}', with id=${message.msg.id}`);
      });
  }

  testPublish(): void {

    this.messaging.publishForSatisfy(
      new NameAgeMessage({ id: 123 }),
      (msg, satisfyArg) => {
      });
  }
}

Answer №1

From my understanding, passing in the string is necessary in this scenario because

this.messaging.subscribe(someCallback);

does not specify which AppMessage types are suitable for the callback. Trying to inspect the argument passed in for someCallback during runtime is likely ineffective as it's simply a JavaScript function object. TypeScript lacks runtime type reflection capabilities and any parameter type information provided by TypeScript will be discarded during compilation.

Possibly enhancing the callback with a messageType property containing relevant information could be an option. However, if that approach is taken, considering two parameters for subscribe() might be simpler:

this.messaging.subscribe(messageType, someCallback);

This current setup involves where messageType is a string compared against the messageType property of the respective AppMessage. An alternative could involve making messageType a class constructor and performing an instanceof check or something similar. But, let's focus on the assumption that messageType is just a string.


To add strong typing to your existing code, omitting manual specification of generic type arguments is usually preferred. Things tend to flow more smoothly when these types can be inferred from the arguments. Therefore, all you need to do is annotate your callback parameter:

this.messaging.subscribe("some unknown thing", 
  (message: AppMessage<{ c: 3 }, { d: 4 }>) => {
    console.log(message.msg.c);
  }
);
// (method) MessagingService.subscribe<{ c: 3; }, { d: 4;}>


Alternatively, assigning all your known AppMessage classes messageType properties of string literal types might simplify the workflow:

class NameAgeMessage extends AppMessage<NameAgeMsg, NameAgeSatisfyArg> {
  public static readonly MESSAGE_TYPE = 'name-age-message'; // string literal type
  public readonly messageType = NameAgeMessage.MESSAGE_TYPE;
  constructor( public readonly msg: NameAgeMsg ) { super(); }
}
class OtherMessage extends AppMessage<{ a: 1 }, { b: 2 }> {
  public static readonly MESSAGE_TYPE = 'other-message';
  public readonly messageType = OtherMessage.MESSAGE_TYPE;
  constructor( public readonly msg: { a: 1 } ) { super(); }
}
⋯

Subsequently, creating a union of these class types allows the compiler to easily look up the matching class based on the string:

type KnownAppMessages = NameAgeMessage | OtherMessage | ⋯

Following this, method overloading the MessagingService subscribe() function would prioritize looking up whenever possible:

class MessagingService {

  ⋯

  // first overload, look up messageType
  public subscribe<K extends keyof KnownAppMessageMap>(
    messageType: K, observer: (message: KnownAppMessageMap[K]) => void
  ): Subscription;

  // second overload, rely on observer parameter annotations
  public subscribe<TMsg, TSatisfyArg>(
    messageType: string, observer: (message: AppMessage<TMsg, TSatisfyArg>) => void
  ): Subscription;

  // impl
  public subscribe<TMsg, TSatisfyArg>(
    messageType: string,
    observer: (message: AppMessage<TMsg, TSatisfyArg>) => void): Subscription {

    const messagesFiltered = this.messages
      .pipe(filter((message) => message.messageType === messageType));

    return messagesFiltered.subscribe(observer as any);
  }

As a result, no annotation of the callback parameter is necessary:

this.messaging.subscribe(
  NameAgeMessage.MESSAGE_TYPE,
  (message) => {
    console.log(`received message type '${message.messageType}', with id=${message.msg.id}`);
  });

Playground link to code

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

Defining Multiple Types in Typescript

I have created an interface in a TypeScript definition file named d.ts: declare module myModule { interface IQedModel { field: string | string[]; operator: string; } } In an Angular controller, I have utilized this interface like ...

Bind the change event of an Angular input to a variable that contains a custom function

Is there a way to achieve something similar to the following? <input (input)="doSomething($event)" /> <input (input)="boolVar = $event.target.value > 5" /> I would like to accomplish it by defining a function, like this: funcVar = (e) =&g ...

What is preventing me from accessing a JavaScript object property when using a reactive statement in Svelte 3?

Recently, while working on a project with Svelte 3, I encountered this interesting piece of code: REPL: <script lang="ts"> const players = { men: { john: "high", bob: "low", }, }; // const pl ...

Struggling to properly test the functionality of my NgForm call in Angular2+

I've been trying to test the login functionality by inputting username and password in an NgForm, but I keep encountering unsuccessful attempts. Is there a vital step that I may be overlooking? Currently, I'm facing this error message: Chrome 6 ...

Leveraging Services in Classes Outside of Angular's Scope

I have a scenario where I am working with Angular v7.3.5 and I need to utilize a Service in a non-Angular class. Below is an example of what I'm trying to achieve: foo.model.ts import { Foo2Service } from './foo2.service'; // Definition fo ...

angular 6 personalized material icons with ligature assistance

Can I create my own custom material icons with ligature support? Currently, I use svgIcon to get Custom Icons, Is there a way to make custom icons that support ligatures? Here is my current code snippet: app.component.ts import { Component } from &ap ...

What causes different errors to occur in TypeScript even when the codes look alike?

type Convert<T> = { [P in keyof T]: T[P] extends string ? number : T[P] } function customTest<T, R extends Convert<T>>(target: T): R { return target as any } interface Foo { x: number y: (_: any) => void } const foo: Foo = c ...

The compiler is unable to locate the node_module (Error: "Module name not found")

Error: src/app/app.component.ts:4:12 - error TS2591: Cannot find name 'module'. Do you need to install type definitions for node? Try npm i @types/node and then add node to the types field in your tsconfig. 4 moduleId: module.id, When att ...

The error message "Declaration file for module 'mime' not found" was issued when trying to pnpm firebase app

Currently, I am in the process of transitioning from yarn to pnpm within my turborepo monorepo setup. However, I have run into an issue while executing lint or build commands: ../../node_modules/.pnpm/@<a href="/cdn-cgi/l/email-protection" class="__cf_e ...

Leveraging jest for handling glob imports in files

My setup involves using webpack along with the webpack-import-glob-loader to import files utilizing a glob pattern. In one of my files (src/store/resources/module.ts), I have the following line: import '../../modules/resources/providers/**/*.resource. ...

Angular Appreciation Meter

Looking to create a rating system using Angular. The square should turn green if there are more likes than dislikes, and red vice versa (check out the stackblitz link for reference). Check it out here: View demo I've tried debugging my code with con ...

Unable to perform module augmentation in TypeScript

Following the guidelines provided, I successfully added proper typings to my react-i18next setup. The instructions can be found at: However, upon creating the react-i18next.d.ts file, I encountered errors concerning unexported members within the react-i18 ...

JavaScript: Translating Date into Moment

Is there a way to convert a Date object to Moment in JavaScript? let testDate = new Date(2020, 05, 03, 1, 2); I attempted the following code without success toMoment(testDate) What is the correct syntax to achieve this conversion? ...

The ngtools/webpack error is indicating that the app.module.ngfactory is missing

I'm currently attempting to utilize the @ngtools/webpack plugin in webpack 2 to create an Ahead-of-Time (AoT) version of my Angular 4 application. However, I am struggling to grasp the output generated by this plugin. Specifically, I have set up a ma ...

Issues arise in TypeScript 5.1.3 with lodash due to type errors involving excessively deep type instantiation, which may potentially be infinite

Recently, I made updates to my Ionic/Angular project and now it is running Angular version 16.1.3 along with TypeScript version 5.1.3. In addition to this, my project also includes the following dependencies: "lodash-es": "^4.17.21", ...

Creating a sidebar in Jupyter Lab for enhanced development features

Hi there! Recently, I've been working on putting together a custom sidebar. During my research, I stumbled upon the code snippet below which supposedly helps in creating a simple sidebar. Unfortunately, I am facing some issues with it and would greatl ...

Error in TypeScript when accessing object using string variable as index

Currently, I am facing a challenge in my project where I am dynamically generating routes and managing them in an Elysia(~Express) application. The issue arises when TypeScript's type checking system fails to index an object using a string variable. S ...

What is the best way to merge an array into a single object?

I have an array object structured like this. [ { "name": "name1", "type": "type1", "car": "car1", "speed": 1 }, { "name": &q ...

Is it possible to get intellisense for Javascript in Visual Studio Code even without using typings?

Is it possible to have intellisense support in Visual Studio Code for a 3rd party library installed via npm, even if there is no typings file available? I have noticed this feature working in IntelliJ/Webstorm, so I believe it might be feasible. However, ...

Tips on how to access the names of all properties within a TypeScript class while excluding any methods

Looking to enhance the reactivity of my code, I want to render my view based on the properties of a class. How can I extract only the property names from a class and exclude methods? For instance: export class Customer { customerNumber: string; ...