Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to declare generic class generic?

I m trying to design a way to declare a function that can return a Promise or a rxjs Observable or a most Stream with the specific return type but I don't know a proper way to declare this function in typescript.

In general I want in my code something like this:

class Action<T> {
    dispatcher: Function
    constructor(dist) {
        this.dispatcher = dist
    }

    f = <T>(s: string): T<string> => this.dispatcher('hello'+s)
}

In this way I get the error: 'T is not generic'

for me T can be a Promise or a Observable or a Stream

If I was able to have something similar to this I can declare a specific dispatcher that return the value how I want it based on how I declared it

const promiseDispacher = (x:any)=>Promise.resolve(x)
const observableDispacher = (x:any)=>Observable.of(x)

What I want to have is the ability of when I call the function f be able to used in different ways

 const a = new Action<Promise>(promiseDistpacher)
 a.f('random string').then((s: string)=>{
   ....
 })

or

 const a = new Action<Observable>(observableDistpacher)
 a.f('random string').subscribe((s: string)=>{
   ....
 })

UPDATED

f is a function that executes some operations and use the dispatcher to wrap the result in the specific T (Promise or Stream) and returned back to the class that executes the function

like image 635
Ricardo Avatar asked Apr 13 '26 15:04

Ricardo


1 Answers

I'm going to try to answer this despite not really being sure what you're doing. As a first shot, let's make T the generic type returned by the passed-in dispatcher, something like Promise<any> or Observable<any> or ReadableStream<any>:

class Action<T> {
  dispatcher: (x: any) => T;
  constructor(dist: (x: any) => T) {
    this.dispatcher = dist
  }
  f = (s: string) => this.dispatcher('hello' + s);
}

const promiseDispatcher = (x: any) => Promise.resolve(x)
const a = new Action(promiseDispatcher);
a.f("random string").then((s: string) => {
  // ...
}); // works

This works now, and the type of f for an Action<T> is inferred to be of type (s: string) => T, and since a is an Action<Promise<any>>, then a.f(s) is a Promise<any>. Note that despite f having a T in its return type, f itself is not what you'd consider a generic function. The generic parameter is on the class, and once you have a concrete instance of the class, its f function is a concrete function type also.


One issue here is that you probably don't really want a.f(s) to be a Promise<any>, but would rather see a Promise<string>. It turns out that since TypeScript doesn't currently support higher kinded types very much, there's no general way to say to make T something like Promise or Observable or ReadableStream, which are themselves generic types. With conditional types you can pick a few hardcoded types like Promise<any>, Observable<any>, and ReadableStream<any> and convert them to Promise<string>, Observable<string>, and ReadableStream<string>, respectively:

type AppliedToString<T> =
  T extends Promise<any> ? Promise<string> :
  T extends Observable<any> ? Observable<string> :
  T extends ReadableStream<any> ? ReadableStream<string> :
  T;

Now if we constrain T to those hardcoded types and use the type function above, we should get something like the typing you want for f:

// constrain T
class Action<T extends Promise<any> | Observable<any> | ReadableStream<any>> {
  dispatcher: (x: any) => T;
  constructor(dist: (x: any) => T) {
    this.dispatcher = dist
  }
  // assert return type of f as AppliedToString<T>
  f = (s: string) => this.dispatcher('hello' + s) as AppliedToString<T>;
}
const promiseDispatcher = (x: any) => Promise.resolve(x);
const a = new Action(promiseDispatcher);
a.f("random string").then((s: string) => {
  // ...
}); // works

Now if you inspect a.f's return type you will see it is a Promise<string>.

The downside to doing it this way is you need to maintain a hardcoded list, and it's also not particularly typesafe, since it will allow you to pass in a dispatcher that returns a Promise<number> and treat it like a Promise<string>... which will explode at runtime.


If I were trying to be simpler and more typesafe, I'd limit T to Promise<string>, Observable<string>, or ReadableStream<string> and forget the mapping, but require the passed-in dispatcher to take a string and return a T:

class Action<T extends Promise<string> | Observable<string> | ReadableStream<string>> {
  dispatcher: (x: string) => T;
  constructor(dist: (x: string) => T) {
    this.dispatcher = dist
  }
  f = (s: string) => this.dispatcher('hello' + s);
}
// note that x is type string in this:
const promiseDispatcher = (x: string) => Promise.resolve(x)
const a = new Action(promiseDispatcher);
a.f("random string").then((s: string) => {
  // ...
})

Okay, hope that one or more of those helps you. Good luck!

like image 178
jcalz Avatar answered Apr 16 '26 08:04

jcalz