Extending abstract ApiServiceBase

To make it easier to build services, I built an abstract class with helper methods for connecting to my API:

import { Headers } from '@angular/http';

export abstract class ApiServiceBase {
  static apiHost = 'https://localhost:3000';

  constructor() {}

  public applyHeaders(headers: Headers=new Headers()) {
    headers.append('Content-Type', 'application/json');
    headers.append('Accept', 'application/json');
    headers.append('Authorization', 'Bearer ' + localStorage.getItem('authToken'));
    return headers;
  }

  public buildUrl(path: string) {
    console.log(`Using ${this.apiHost} to build url for ${path}`);
    return this.apiHost + path + '.json';
  }
}

When I try to extend the method, like the following, I get a number of errors:

...
import { ApiServiceBase } from 'api-service-base';

@Injectable
export class TripService extends ApiServiceBase {
  constructor(public http: Http) { super(); }

  get() {
    return new Promise((resolve, reject) => {
      this.http.get(this.buildUrl('/trips'), { headers: this.applyHeaders() })
        .map(res => res.json())
        .subscribe(trips => {
          let tripArr: [Trip] = trips.map(trip => new Trip(trip));
          resolve(tripArr);
        }, error => reject(error));
    });
  }
}

The errors I get:

  • On ApiServiceBase from export class TripService extends ApiServiceBase I get Type 'any' is not a constructor function type
  • On calls to buildUrl I get Property 'buildUrl' does not exist on type 'TripService'
  • On calls to applyHeaders I get Property 'applyHeaders' does not exist on 'TripService'

This is a project that I’m trying to upgrade from Ionic 2 beta 11 to Ionic 2.1.0 release and it worked just fine on Ionic 2 beta 11.

If you don’t get any better answers, I would rearchitect this to use composition instead of inheritance. Largely due to the lack of runtime class information in objects, I think inheritance in TypeScript is a bit of a minefield.

Mind helping me with the composition logic? In other languages, I generally do something to include the methods from the APIBaseService into my Services, but I’m not super familiar with TypeScript.

export class TripService {
  constructor(private _api: ApiService, private _http: Http) {
  }

  get(): Observable<Trip[]> {
    return this._http.get(this._api.buildUrl('/trips'), { headers: this._api.applyHeaders() }).map((rsp) => {
      return rsp.json() as Trip[];
    });
  }
}

Everywhere you’d be using a superclass function, you delegate explicitly to the ApiService. I actually find this much more readable than using inheritance as it’s instantly clear where various functions are defined. Also notice no need for instantiating Promises: I know that boilerplate code is omnipresent, but I’ve railed against it several times for being antipattern stew.

You are explicitly constructing Trip objects. If you still need to do that, fine, but what I do is to simply make all of my objects that go over the wire interfaces, at which point either single ones or arrays of them just reanimate with a single json()call.

So I switched to having ApiService as a standard class, added it to my providers definition in app.module.ts and am trying to inject it into my other services. I don’t get any errors on imports, but on my constructor, which now is:

import { ApiService } from 'api-service';

constructor(public http: Http, _api: ApiService) {...}

I get an error Cannot find name 'ApiService'. on the constructor portion. The import doesn’t error at all, so I’m not sure what I’m missing.

Is ApiService also decorated as @Injectable()? Also note that you’re going to need an access qualifier on _api in order to be able to access it outside of that constructor.

This is now my ApiService:

import { Injectable } from '@angular/core';
import { Headers } from '@angular/http';

@Injectable()
export class ApiService {
  apiHost = 'http://localhost:3000';

  applyHeaders(headers: Headers = new Headers()) {
    headers.append('Content-Type', 'application/json');
    headers.append('Accept', 'application/json');
    headers.append('Authorization', 'Bearer ' + localStorage.getItem('authToken'));
    headers.append('User-Agent', 'Go Rudys ' + navigator.userAgent)
    return headers;
  }

  buildUrl(path: string) {
    console.log(`Using ${this.apiHost} to build url for ${path}`);
    return this.apiHost + path + '.json';
  }
}

And one of my services that uses it:

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import 'rxjs/add/operator/map';
import { ApiService } from 'api-service';

@Injectable()
export class AirlineService {
  data: any;

  constructor(private http: Http, private _api: ApiService) {
    this.data = null;
  }

  load() {
    // already loaded data
    if (this.data) { return Promise.resolve(this.data); }

    // don't have the data yet
    return new Promise(resolve => {
      this.http.get(this._api.buildUrl('/airlines'), { headers: this._api.applyHeaders() })
        .map(res => res.json())
        .subscribe(data => {
          this.data = data.map(airline => airline as Airline);
          resolve(this.data);
        });
    });
  }
}

And the error I get from ionic build:

[16:01:42]  typescript: src/providers/airline-service.ts, line: 10
            Cannot find name 'ApiService'.

      L10:    constructor(private http: Http, private _api: ApiService) {
      L11:      this.data = null;

Try making that from ‘./api-service’ instead.

That worked. Thanks @rapropos!