Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 Typescript: Is it possible to pass an interface as a parameter into a function?

I have the following problem: I pull data from a JSON API. I have at the moment a service for every data model (eg. articles, users, etc) and a model class for every data model. But this is insane and not really maintainable. So I would like to refactor so that I have an interface and a model class for every data model and one unified DataAPIService.

The problem is, that the functions in DataAPIService that query the API should not return JSON but objects or collections of objects of the type that has been queried. So I need a way to pass the interface or type into the query method of the service to then initialize a new object of this type.

Is this possible? Do I make sense? Here is some code to help understand what I mean and show my current progress.

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

import { AuthHttp } from 'angular2-jwt';
import 'rxjs/add/operator/map';

import { Config } from '../config/env.config';

@Injectable()
export class DataAPIService {

  constructor(
    private authHttp: AuthHttp
  ) {}

  // This function will be called to retrieve data (for example from a Component). 
  // I want to pass in the object type or interface so that I only have one 
  // getIndex() function and not one for every data type.

  getIndex(page:number = 1, object_name:string, object_type) {
    return this.authHttp.get(Config.API_ENDPOINT + '/' + object_name + '?page=' + page)
      .map(res => res.json())
      .map(res => {
        return this.fromJson(res.data, object_type);
      });
  }

  // This function just checks which attributes are present in the object type 
  // and fills a new object of this type with the values from JSON.  
  // This is partly pseudo-code since I don't know how to solve this.

  fromJson(input_json: any, object_type) {
    // The next line is obviously not working. This is what I'm trying to figure out
    var object:object_type = new object_type();
    var json_attributes = input_json.attributes;
    for (var key in json_attributes) {
      if (object.hasOwnProperty(key)) {
        object[key] = json_attributes[key];
      }
    }
    object.id = input_json.id;
    return object;
  }

}
like image 363
Ole Spaarmann Avatar asked Oct 27 '16 13:10

Ole Spaarmann


2 Answers

What you could do is work with generics (if you do not know what these are, I recommend googling this).

@Injectable()
export class DataAPIService {

  constructor(
    private authHttp: AuthHttp
  ) {}

  // This function will be called to retrieve data (for example from a     Component). 
  // I want to pass in the object type or interface so that I only have one 
  // getIndex() function and not one for every data type.

  getIndex<T>(page:number = 1, object_name:string): Observable<T> {
    return this.authHttp.get(Config.API_ENDPOINT + '/' + object_name +     '?page=' + page)
      .map(res => res.json());
  }

By just adding the T generic to your method, you can define the return type to be an Observable of values with type T. The res.json() will just create an object and if it is returned to the caller of this method, he'll just see an observable of values with type T. No need to write such a specific parsing function to an interface.

like image 71
KwintenP Avatar answered Oct 16 '22 02:10

KwintenP


This is how I have solved the whole thing. It was important for me that the resulting object is not a generic object but for example of the type Post. I also wanted to use interfaces and I wanted the initialization of the object to be easy.

First, I have a base class from which all data models inherit.

base-model.model.ts

import * as _ from 'lodash';

export class BaseModel {

  public id: string;
  [key: string]: any;

  constructor(private data?: any) {
    // This basically does the initialization from a variable json object. 
    // I later on just pass the json data into the constructor.
    if (data) {
      this.id = data.id;
      _.extend(this, data.attributes);
    }
  }
}

Now the actual model that inherits from the Base Model:

member.model.ts

// The actual model. It has an interface and extends the base class 
// (so that the main logic is just in one place - DRY)

import { BaseModel } from './base-model.model';

interface MemberInterface {
  email:string;
  name:string;
}

export class Member extends BaseModel implements MemberInterface {

  email:string;
  name:string;

  constructor(data?: any) {
    super(data);
  }

}

Let's use it. With a Service that pulls data from an API

import { Injectable } from '@angular/core';
import { AuthHttp } from 'angular2-jwt';
import 'rxjs/add/operator/map';
import { Config } from '../config/env.config';

@Injectable()
export class MemberService {

  constructor(public authHttp: AuthHttp) {}

  // Calls the API and returns the result.
  // authHttp works very similar to http. Just with added JWT protection
  // check it out on GitHub: angular2-jwt
  getIndex(page:number = 1):any {
    let url = [Config.API_ENDPOINT, 'members', '?page='].join('/');
    return this.authHttp.get(url + page)
      .map(res => res.json())
      .map(res => {
        return res;
      });
  }

  // Simpler example when just getting one entry
  getOne(id: string):any {
    let url = [Config.API_ENDPOINT, 'members', id].join('/');
    return this.authHttp.get(url)
      .map(res => res.json())
      .map(res => {
        return res;
      });
  }


}

And finally let's use the Model class and the Service together

import { Component, OnInit } from '@angular/core';

import { MemberService } from '../shared/index';
import { Member } from '../shared/models/member.model';

@Component({
  moduleId: module.id,
  selector: 'app-member-list',
  templateUrl: 'member-list.component.html',
  styleUrls: ['member-list.component.css']
})
export class MemberListComponent implements OnInit {

  private members: Array<Member>;
  private member: Member;

  constructor(private memberService: MemberService) {
    this.members = [];
    this.member = new Member();
  }

  ngOnInit():any {
    // Pull the list on initialization
    this.getIndex(1);
  }

  // For the index
  getIndex(page:number = 1):Array<Member> {
    this.memberService.getIndex(page).subscribe(
      res => {
        this.members = [];
        for(let i = 0; i < res.data.length; i++) {
          let member = new Member(res.data[i]);
          this.members.push(member);
        }
      },
      err => console.log(err)
    );
  }

  // Simpler version with just one entry 
  getOne():any {
    this.memberService.getIndex(page).subscribe(
      res => {
        this.member = new Member(res.data.attributes);
      },
      err => console.log(err)
    );
  }

}
like image 42
Ole Spaarmann Avatar answered Oct 16 '22 02:10

Ole Spaarmann