8

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:

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.

Shane
  • 827
  • 8
  • 18
  • Passing the constructor _is_ the correct way but you are correct that it is awkward (although quite possible) to do that with Angular's DI abstraction. Have you considered replacing the generic class with a generic method? – Aluan Haddad Apr 05 '18 at 05:47
  • @AluanHaddad Not really, what would that look like? – Shane Apr 05 '18 at 05:50
  • Like like `class S { get(C: new (x: Thing) => T): Promise {...} }`. But I see now that you are using the constructor itself to trigger initialization of an asynchronous process that then changes application level state. That is to say that your service is working as a side-effect of the service being injected (and thus instantiated) by something that required it... – Aluan Haddad Apr 05 '18 at 05:53
  • @AluanHaddad Hmmm, yeah, Probably shouldn't be doing async things in a constructor. Reading between the lines, I think you're saying that I've made a couple of design mistakes that are causing my problem here? – Shane Apr 05 '18 at 05:56
  • Well... yeah I'm suggesting that you might have. It's hard to say since you may have a use case that I've not considered. What irks me is that the generic parameter would only make sense on the service class if there were exactly one instance created for each distinct type parameter used with it. Is it a singleton or are there multiple instances in an app? – Aluan Haddad Apr 05 '18 at 05:59
  • @AluanHaddad IIRC, the DI system creates one instance per each type used throughout the app. – Shane Apr 05 '18 at 06:41
  • It depends on how it is provided. You can provide services at component level, app level, and other ways as well (lazy shared feature modules get different scopes). – Aluan Haddad Apr 05 '18 at 06:47

1 Answers1

1

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);
        });
    }
} 
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • I'm using Angular DI. This does not. When I've tried to adapt what you have here, it fails. `Can't resolve all parameters for tService: ([object Object], ?).` – Shane Apr 05 '18 at 06:43
  • @Shane someone will need to pass in the constructor for `T`. I am not sure who Angular DI deals with generics, but since generics are just a compile type construct, i think they just create a new `tService` and they don't care about the `T` – Titian Cernicova-Dragomir Apr 05 '18 at 06:46
  • @Shane added some options, but you will need to pass the constructor for `T` somehow, typescript will not know what to do with `new T` – Titian Cernicova-Dragomir Apr 05 '18 at 06:56