Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript generics in a callback

I'm trying to make a generic service that will get some remote data and create some objects with it.

@Injectable()
export class tService<T> {
    private _data: BehaviorSubject<T[]> = new BehaviorSubject([]);
    private url = '...url...';  // URL to web api

    constructor(private http: HttpClient) {    
        this.http.get<T[]>(this.url).subscribe(theThings => {
            theThings = _.map(theThings, (theThing) => new T(theThing));
            this._data.next(theThings);
        });
    }
}

This gives me the error T only refers to type, but is being used as a value here. Ok, that's fine. I understand what is happening. I've seen a couple of questions asking about something similar.

For Example:

  • How to create a new object from type parameter in generic class in typescript?
  • TypeScript return object constructed form generic
  • Get constructor/instance from generic type in TypeScript

It seems that any solution that I've come across either hardcodes in the class at some point, or, adds T's class constructor in somewhere. But I can't figure out how to do it. My problem is that the class being instantiated has parameters in the constructor and I'm using Angular's DI system, so I can't (???) add parmeters to the service's constructor.

constructor(private playersService: gService<Player>, private alliesService: gService<Ally>) { }

Can anyone figure out what I should do here? Also, I'd prefer for an answer to not be some sort of 'hack', if possible. If it a choice between doing something that is barely readable and just copy and pasting the service a couple of times and changing what class it is referring to, I'll take the second option.

like image 488
Shane Avatar asked Apr 05 '18 05:04

Shane


People also ask

Does TypeScript support generics?

TypeScript fully supports generics as a way to introduce type-safety into components that accept arguments and return values whose type will be indeterminate until they are consumed later in your code.

How do you pass a callback function in TypeScript?

Similar to JavaScript, to pass a function as a parameter in TypeScript, define a function expecting a parameter that will receive the callback function, then trigger the callback function inside the parent function.

What is callback in TypeScript?

A callback is a function passed as an argument to another function. This technique allows a function to call another function. A callback function can run after another function has finished.

What is the use of generics in TypeScript?

Generics allow creating 'type variables' which can be used to create classes, functions & type aliases that don't need to explicitly define the types that they use. Generics makes it easier to write reusable code.


1 Answers

A class name refers to both the of the class type but also to it's constructor, that is why you can write both let x: ClassName and new ClasName(). A generic parameter is only a type (much like an interface for example), that is why the compiler complains that you are using it as a value (the value being expected being a constructor function). What you need to do is pass an extra parameter that will be the constructor for T:

export class tService<T> {
    private _data: BehaviorSubject<T[]> = new BehaviorSubject<T>([]);
    private url = '...url...';  // URL to web api

    constructor(private http: HttpClient, ctor: new (data: Partial<T>)=> T) {    
        this.http.get<T[]>(this.url).subscribe(theThings => {
            theThings = _.map(theThings, (theThing) => new ctor(theThing));
            this._data.next(theThings);
        });
    }
}
//Usage
class MyClass {
    constructor(p: Partial<MyClass>) {
        // ...
    }
}
new tService(http, MyClass)

Note The parameters to the constructor signature may vary according to your use case.

Edit

You mention that you can't add arguments to the constructor, generics (and all types in general) are erased when we run the code, so you can't depend on T at runtime, somewhere, someone will have to pass in the class constructor. you can so this in several ways, the simplest version would be to create dedicated classes for each T instance, and then inject the specific class:

class tServiceForMyClass extends tService<MyClass> {
    constructor(http: HttpClient)  {
        super(http, MyClass)
    }
} 

Or you could do your work dependent on the constructor, in an init method that requires the constructor as a parameter:

export class tService<T> {
    private _data: BehaviorSubject<T[]> = new BehaviorSubject<T>();
    private url = '...url...';  // URL to web api

    constructor(private http: HttpClient){}
    init(ctor: new (data: Partial<T>)=> T) {    
        this.http.get<T[]>(this.url).subscribe(theThings => {
            theThings = _.map(theThings, (theThing) => new ctor(theThing));
            this._data.next(theThings);
        });
    }
} 
like image 52
Titian Cernicova-Dragomir Avatar answered Sep 21 '22 16:09

Titian Cernicova-Dragomir