Pourquoi typer les réponses HTTP ?

Par défaut, la méthode HttpClient.get() parse les données JSON renvoyées par le serveur en un type Object anonyme. Il existe pourtant une autre syntaxe qui permet de typer explicitement les données renvoyées. Comment l’utiliser et pourquoi ?

Réponse HTTP non typée (fonctionnement par défaut)

Reprenons l’exemple de la doc officielle de HttpClient. On a un service de configuration qui récupère le contenu d’un fichier JSON sur le serveur.

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

@Injectable()
export class ConfigService {
  configUrl = 'assets/config.json';

  constructor(private http: HttpClient) { }

  getConfig() {
    // Les données renvoyées par HTTP GET seront non typées.
    return this.http.get(this.configUrl);
  }
}

Contenu de assets/config.json :

{
  "heroesUrl": "api/heroes",
  "textfile": "assets/textfile.txt"
}

Dans cet exemple, les données JSON renvoyées par http.get() ne sont pas typées et donc parsées en un type Object anonyme. Autrement dit, le compilateur (et l’IDE) ne connaît pas la forme des données renvoyées.

Conséquence : si on tente d’utiliser les données renvoyées par le ConfigService de la manière suivante, ça plante :

this.configService.getConfig()
  .subscribe(data => {
     const url = data.heroesUrl;  // ERREUR
  });

Pour le compilateur, data est juste un objet “opaque”. Il ne sait pas que la propriété data.heroesUrl existe puisqu’on n’a pas typé la réponse HTTP. Une manière d’éviter cette erreur est d’accéder à la propriété heroesUrl avec la syntaxe crochets obj[“prop”], ce qui permet d’éviter le contrôle de type :

this.configService.getConfig()
  .subscribe(data => {
     const url = data['heroesUrl'];  // OK, car syntaxe crochets
  });

Réponse HTTP typée (assertion de type)

Une autre manière d’éviter l’erreur précédente est de typer les données renvoyées par la requête HTTP.

Notre requête devient :

  getConfig() {
    // Les données renvoyées par HTTP GET sont maintenant typées avec `Config`
    return this.http.get<Config>(this.configUrl);
  }

Config est une interface qui décrit la forme des données renvoyées par le serveur :

export interface Config {
  heroesUrl: string;
  textfile: string;
}

Quand on récupère les données du serveur, on peut maintenant accéder aux propriétés heroesUrl et textfile sans problème, puisque leur existence est documentée par le typage.

this.configService.getConfig()
  .subscribe(data => {
     const url = data.heroesUrl;  // OK
     const file = data.textfile;  // OK
  });

Ré-instancier les données de la réponse HTTP

Il est essentiel de comprendre que la syntaxe HttpClient.get<TYPE> ne transforme pas les données renvoyées par le serveur. Prenons un exemple pour bien ancrer ce point.

Imaginons une classe Quiz qui représente un modèle de données de l’application :

export class Quiz {
   constructor(public title: string,
               public level: 'easy' | 'medium' | 'hard',
               public isPublished = false,
               ...) { }
}

On peut créer une instance de Quiz avec du code comme celui-ci :

const quiz = new Quiz('Mon quiz', 'easy', true...);

Supposons maintenant qu’on veuille charger une liste de quizzes depuis le backend, et que donc on type la réponse de la requête HTTP avec le type Quiz[] :

this.http.get<Quiz[]>('/api/quizzes')
  .subscribe(data => ...);

Que pensez-vous qu’il y a dans data ?…

La bonne réponse est “on ne le sait pas avec certitude, mais le compilateur TypeScript va considérer que c’est un tableau de trucs qui ressemblent à des quizzes”. Dans notre code, on pourra donc manipuler la variable data exactement comme si elle contenait un tableau de quizzes (même si ce n’est pas vraiment le cas).

Par exemple, pour récupérer le titre du premier quiz, on pourrait écrire :

this.http.get<Quiz[]>('/api/quizzes')
  .subscribe(data => {
    const firstQuizTitle = data[0].title;  // OK, on a un tableau d'objets qui ont une propriété `title`
  });

L’erreur serait de penser que data contient un tableau d’instances de Quiz. C’est faux ! La syntaxe HttpClient.get<TYPE> fait juste une assertion de type sur les données renvoyées par le serveur, sans les transformer. Ainsi, cette technique est trompeuse car elle donne l’impression que certaines propriétés existent sur les données (côté code), alors qu’elles pourraient très bien être absentes du JSON sous-jacent…

Si vous voulez récupérer des vraies instances de quizzes, il faut faire des new Quiz() manuellement. Par exemple :

this.http.get<Quiz[]>('/api/quizzes')
  // Transforme les données brutes en instances de quizzes
  .pipe(
    map(data => data.map(quizData => new Quiz(quizData)))
  )
  .subscribe(quizzes => { /* ICI, on a bien des instances de quizzes */ });

Dans ce dernier exemple, la propriété quizzes finale contient bien un tableau d’instances de Quiz.

Que choisir : assertion de type ou instanciation de classe ?

L’assertion de type sur les données renvoyées par une requête HTTP (syntaxe HttpClient.get<TYPE>) a plusieurs avantages :

  • Elle est facile à utiliser. Une simple interface sert à décrire les données.
  • Elle rend le code de la requête plus lisible ; on comprend exactement ce que le serveur est censé renvoyer.
  • Elle permet d’aller vite en phase de développement.

Mais il faut garder en tête que c’est une technique un peu bidon, qui “trompe” le compilateur sur la vraie nature des données.

CONCLUSION : Utilisez cette technique plutôt pour les données pures/simples, par exemple les paramètres de configuration de l’application.

D’un autre côté, l’instanciation manuelle d’objets est beaucoup plus robuste :

  • C’est une manière de contrôler l’intégrité des données. Si le backend omet certaines propriétés, l’instanciation pourrait planter, ce qui éviterait à l’application de travailler sur des données partielles ou corrompues.
  • La classe peut initialiser certaines propriétés avec une valeur par défaut, ou calculer des propriétés dynamiquement avec l’opérateur get.
  • Les instances de classe peuvent posséder des méthodes qu’on va pouvoir utiliser dans le reste de l’application.

Les inconvénients sont une syntaxe un peu plus complexe et la nécessité d’avoir une classe pour créer les instances.

CONCLUSION : Utilisez cette technique plutôt pour les “modèles” de l’application.

Informations

Tags : http

Dernière mise à jour :

Auteur : AngularChef

Qualité : Bonne