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.

7
  • 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? Commented Apr 5, 2018 at 5:47
  • @AluanHaddad Not really, what would that look like? Commented Apr 5, 2018 at 5:50
  • Like like class S { get<T>(C: new (x: Thing) => T): Promise<T> {...} }. 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... Commented Apr 5, 2018 at 5: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? Commented Apr 5, 2018 at 5: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? Commented Apr 5, 2018 at 5:59

1 Answer 1

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);
        });
    }
} 
Sign up to request clarification or add additional context in comments.

3 Comments

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 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
@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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.