Search code examples
angulartypescriptrestgoogle-news

Is there a way to map complex json data onto interfaces in typescript while using observables?


I am trying to make simple web news reader, based mostly on google news api (https://newsapi.org/s/google-news-api). I have done everything like always so services, models etc. but i have problem with mapping my data onto concrete interfaces. Google api returns on one of the endpoint ('top-headlines') data in this format:

{
    "status": "ok",
    "totalResults": 38,
    "articles": [
       {
       "source": {
            "id": "the-new-york-times",
            "name": "The New York Times"
       },
      "author": null,
      "title": "Hong Kong Protesters Clash with Police Amid Fears of Mob 
       Violence - The New York Times",
      "description": "some description",
      "url": "https://www.nytimes.com/2019/08/11/world/asia/hong-kong- 
       protest.html",
      "urlToImage": 
      "https://static01.nyt.com/images/2019/08/11/world/11hongkong15/
       11hongkong15-facebookJumbo.jpg",
      "publishedAt": "2019-08-11T11:19:03Z",
      "content": "some content… [+751 chars]"
       },
      (...and many many more articles)
    ]
}

I need only articles. And i know that if i get this data from api like that i can call myvariablename.articles, but this does not seems right to me.

My attempt on first was to map in with rxjs map method. But i get error in console that there are is no property articles in Articles model. So I prepared response model and nested articles as property inside response model (interface). And i get exacly the same error. I was thinking that maybe there is some problem with defined in method Observable so i swiched to Response but with no luck, also right now i am getting error from my component where i am subscribing and using service method which says that: "Type Response[] is not assignable to type Article[]"

top-news.service.ts

(...all imports)

export class TopNewsService {

(... ommited defining of variables like url etc.)

  public getAll(): Observable<Response[]> {
    return this.httpClient
      .get<Response[]>(`${this.url}/${this.endpoint}?country=us&apiKey=${this.token}`)
      .pipe(
        map(response => response.articles),
        catchError(this.handleError)
      );
  }
(...error handling)
}

article.ts

export interface Article {
  source: {
    id: string;
    name: string;
  };
  author: string;
  title: string;
  description: string;
  url: string;
  urlToImage: string;
  publishedAt: string;
  content: string;
}

response.ts

import { Article } from './article';

export interface Response {
  status: string;
  totalResults: number;
  articles: Article;
}

top-news-component.ts

(...all imports)

export class TopNewsComponent implements OnInit {

  articles: Article[];

  constructor(private http: HttpClient, private topNewsService: TopNewsService) { }

  ngOnInit() {
    this.getAllProjects();
  }

  getAllProjects() {
    this.topNewsService.getAll().subscribe(data => {
      this.articles = data;
      console.log(this.articles)
    },
      (err: string) => {
        alert(err);
      });
  }
}

I would like to be able to map data inside service and return only articles and then (if this is possible) get this data inside my component and assign it to created variable with defined type Article[].


Solution

  • You're using

    this.httpClient.get<Response[]>
    

    That means that you expect Google to return an array of Response. But that's not what it returns. It returns a single Response. So it should be

    this.httpClient.get<Response>
    

    And since you (try to) map this response to an array of articles, then the observable that your service returns is an Observable<Article[]>, not an Observable<Response[]>. So it should be

    public getAll(): Observable<Article[]> {
    

    This interface

    export interface Response {
      status: string;
      totalResults: number;
      articles: Article;
    }
    

    is also wrong. A response doesn't contain a single article. It contains an array of article. Look at the JSON:

    {
      "status": "ok", // one status string
      "totalResults": 38, // one totalResults number
      "articles": [ // an **array** of articles
        ..
      ]
    }
    

    So it should be

    export interface Response {
      status: string;
      totalResults: number;
      articles: Article[];
    }