Skip to content
Angular2: using TypeScript generics

Angular2: using TypeScript generics

This post is an example of TypeScript generics usage, when writing an Angular2 application.

I'm working on a Stripe administration console. Stripe is a payment / subscription SaaS service that we use at OctoPerf.

I need to create pages that lists all our customers and plans. But Stripe returns paginated lists, and we need to do successive calls to their API to get the complete data. This mechanism is the same for both customers and plans lists. So we can factorise our code using generics in this particular case.

The generic model

The first step is to create a common interface for customers and plans. They share a String id.

export interface HasId {
  id:string;
}

OctoPerf is JMeter on steroids!
Schedule a Demo

We will see later that this id is used while making successive calls to Stripe API, thus the interface.

Then we just need to declare our customer and plan classes:

import {HasId} from './HasId';

export class Customer implements HasId{
  constructor(public email: string, public id:string) {
  }
}

import {HasId} from './HasId';

export class Plan implements HasId{
  constructor(public name: string, public id:string) {
  }
}

The real objects have more members, that we ignore for this example.

Note:

We could also have used interfaces for Customer and Plan instead of classes.

Stripe always returns paginated list. So when you ask for data you may specify:

  • a limit (from one to 100) of number of elements returned,
  • and the starting_after param, which is the ID of the last element of the previous call made to their API.

We declare a paginated list, that uses a generic array of HasId for its data <T extends HasId>:

import {HasId} from './HasId';

export class PaginatedList<T extends HasId> {
  constructor(public has_more: boolean,
              public data:T[]) {
  }
}

The has_more parameter is used to indicate when we reached the end of the overall data.

The generic usage

The StripeClient service lets us retrieve Customers and Plans alike:

import {Injectable} from 'angular2/core';
import {Http, Headers, URLSearchParams} from 'angular2/http';
import {Customer} from './Customer';
import {Plan} from './Plan';
import {PaginatedList} from './PaginatedList';
import {HasId} from './HasId';

const stripeUrl: string = 'https://api.stripe.com/v1';

@Injectable()
export class StripeClient {
  private apiKey: string = '';

  constructor(private http: Http) {
  }

  getApiKey(): string {
    return this.apiKey;
  }

  setApiKey(apiKey: string) {
    this.apiKey = apiKey;
  }

  getCustomers(callback: (customers: PaginatedList<Customer>)=>void) {
    this.getData('/customers', null, callback);
  }

  getPlans(callback: (plans: PaginatedList<Plan>)=>void) {
    this.getData('/plans', null, callback);
  }

  private getData<T extends HasId> (endpoint: string, starting_after: string, callback: (data: PaginatedList<T>)=>void) {
    const params: URLSearchParams = new URLSearchParams();
    params.set('limit', '100');
    if (starting_after){
      params.set('starting_after', starting_after);
    }

    this.http.get(stripeUrl + endpoint, {headers: this.getAuthHeaders(), search: params})
      .map(res => res.json())
      .subscribe((paginatedList: PaginatedList<T>) => {
        callback(paginatedList);
        if (paginatedList.has_more){
          this.getData(endpoint, paginatedList.data[paginatedList.data.length - 1].id, callback);
        }
      });
  }

  private getAuthHeaders(): Headers {
    var headers = new Headers();
    headers.append('Authorization', 'Bearer ' + this.apiKey);
    return headers;
  }
}

The getData method is the core of this service. It recursively calls Stripe API and gives the results to the callback parameter. It uses the type variable <T extends HasId>: T can be replaced by any class or interface that extends HasId. So T can either be Customer or Plan.

Its callback parameter can then also take a paginated list of Plans or Customers. And we use the T type variable when we set the type of the results: .subscribe((paginatedList: PaginatedList<T>) => {...}.

Note:

It's a bit more cumbersome to set headers and query parameters in Angular2 than in AngularJS: you have to create specific objects:

  • new Headers(); in the getAuthHeaders() method so set the authentication headers,
  • new URLSearchParams(); to set the limit and starting_after query parameters.

The getPlans and getCustomers methods hide this complexity to the callers: they use non generic callback and are simply used like this:

@Component({
  ...
})
export class Customers {
  private customers: Customer[];
  private loading: boolean;

  constructor(private client: StripeClient) {
    this.customers = [];
    this.loading = true;
  }

  ngOnInit() {
    console.log('Customers');
    this.client.getCustomers((customers: PaginatedList<Customer>) => {
      this.customers = this.customers.concat(customers.data);
      this.loading = customers.has_more;
    });
  }
}

As the callback may be called more than once (if there are more than a hundred results) we need to concat the results.

To know more about how to unit test this service, take a look at my next article.

Want to become a super load tester?
Request a Demo