Publié par

Il y a 5 années -

Temps de lecture 8 minutes

Angular et TypeScript, un mariage heureux !

Angular est devenu en peu de temps LE framework JavaScript du moment. Bénéficiant d’un "effet waouh" impressionnant, il n’en reste pas moins que ni le framework, ni sa documentation, ne sont parfaits. Une partie des problèmes d’Angular est liée à des choix "logiciels" (le routeur par défaut ou les directives par exemple), l’autre partie est inhérente au langage JavaScript. C’est pour circonvenir à cette deuxième catégorie que nous nous proposons d’étudier TypeScript.

TypeScript fait partie de la ribambelle de langages alternatifs compilant en JavaScript. Ses caractéristiques principales sont le typage statique à la compilation et le fait qu’il soit un super-ensemble de JavaScript, visant à rester le plus proche possible de la futur norme EcmaScript 6 pour la syntaxe.

Mais alors, que peut bien apporter le mélange de ces 2 technologies ? C’est ce que nous allons voir dans la suite.

Mise en place du projet

Le projet que nous allons utiliser comme support se propose d’afficher dans une page web la liste des planètes du système solaire. Vous pouvez le retrouver sur github : https://github.com/blemoine/angular-typescript-planet

Compilation de TypeScript

TypeScript étant un langage compilé vers JavaScript, la première étape pour pouvoir travailler est de mettre en place un système de compilation. Il existe pour cela des plugins dans à peu près toutes les technologies de build du moment, de Maven à Gulp en passant par Grunt.

TypeScript autorise 2 modes de compilation : EcmaScript 3 (compatibilité IE6) et EcmaScript 5 (Compatibilité IE9). Ici, nous ciblerons EcmaScript 5, car sans cela, il nous serait impossible d’utiliser les getter/setter ou encore les méthodes de haut niveau comme filtre ou map.

Un exemple de GruntFile.coffee permettant la compilation est disponible sur le repository github.

Fichier de définitions

TypeScript étant un langage typé statiquement, il est nécessaire de fournir au compilateur un fichier de définition des types lorsque l’on souhaite utiliser une librairie externe. Ce fichier de définition apporte ensuite un confort important dans le développement car il permet à votre IDE de connaître précisement les propriétés disponibles sur un objet. On pourra retrouver un grand nombre de fichiers de définitions pour de très nombreuses librairies sur le repository Definitely Typed

Vos fichiers utilisant la variable globale angular doivent donc maintenant commencer par une référence au fichier de définition d’Angular, disponible plus précisément à l’url : https://github.com/borisyankov/DefinitelyTyped/blob/master/angularjs/angular.d.ts. Ce fichier de définition référence celui de jQuery, il est donc nécessaire de télécharger aussi celui-ci : https://github.com/borisyankov/DefinitelyTyped/blob/master/jquery/jquery.d.ts

Le modèle

La notion de modèle du pattern MV* est quelque peu cachée par Angular. TypeScript permet finalement de faire ressortir cette notion bien mieux en manipulant des objets typés plus fortement.

Nous allons ici créer une interface Planet, et utiliser le fait que TypeScript supporte le typage structurel : il n’est pas obligatoire d’implémenter explicitement une interface si le contrat d’interface est respecté par l’objet.

interface Planet {
    name:string
    isRocky:boolean
}

Initialisation de la page avec un controller

Nous allons maintenant réaliser l’affichage d’une liste de planètes fournie par un controller dans une page. Le controller écrit en TypeScript donnera par exemple :

/// <reference path="../definition/angularjs/angular.d.ts" />
var planetsModule = angular.module('planetsModule',[]);
class PlanetsController {
    planets:Array<Planet> = [];
    constructor() {
        this.planets = [
        {name: 'Mercure', isRocky: true},
        {name: 'Venus', isRocky: true},
        {name: 'Terre', isRocky: true},
        {name: 'Mars', isRocky: true},
        {name: 'Jupiter', isRocky: false},
        {name: 'Saturne', isRocky: false},
        {name: 'Uranus', isRocky: false},
        {name: 'Neptune', isRocky: false}
        ];
    }
}
planetsModule.controller('PlanetsController',PlanetsController);

et son utilisation dans une page html :

<div ng-app="planetsModule">
      <section ng-controller="PlanetsController as planetsController">
        <ul>
         <li ng-repeat="planet in planetsController.planets">{{planet.name}}</li>
        </ul>
      </section>
</div>

On constatera ici 2 choses importantes :

  •  le controller est maintenant, explicitement, une classe et peut donc facilement être étendu et testé. On remarquera que l’on n’utilise pas de $scope pour exposer les données.
  •  les données sont exposées via la syntaxe "controller as", nouvelle en angular 1.2 . Cette syntaxe permet de manipuler réellement une instance de la classe controller plutôt que le scope qui lui est passé en paramètre.

Getter

Nous voulons maintenant filtrer les planètes par leur nom en tapant dans un champ texte. Dans la vraie vie, nous utiliserions un filter angular, mais ici nous allons faire le filtre dans le controller, pour la démonstration.

TypeScript nous permet d’utiliser une syntaxe simplifiée sous forme de getter :

class PlanetsController {

    filter:string = null;
    planets:Array<Planet> = [];

    constructor() {
        this.planets = [
        {name: 'Mercure', isRocky: true},
        {name: 'Venus', isRocky: true},
        {name: 'Terre', isRocky: true},
        {name: 'Mars', isRocky: true},
        {name: 'Jupiter', isRocky: false},
        {name: 'Saturne', isRocky: false},
        {name: 'Uranus', isRocky: false},
        {name: 'Neptune', isRocky: false}
        ];
    }

    get planetsFiltered() {
        if (this.filter) {
            return this.planets.filter((planet) => planet.name.indexOf(this.filter) >= 0)
        }
        return this.planets;
    }
}

Et le template :

 <section ng-controller="PlanetsController as planetsController">
        <input ng-model="planetsController.filter" />
        <ul>
             <li ng-repeat="planet in planetsController.planetsFiltered">{{planet.name}}</li>
        </ul>
</section>

On gagne en lisibilité, et il n’y a toujours pas besoin d’utiliser de scope.

Utilisation d’un service

Supposons maintenant que nos données proviennent d’un service. De la même façon que pour les controllers, on peut utiliser une classe :

class PlanetsService {
    private planets:Array<Planet> = [
        {name: 'mercure', isRocky: true},
        {name: 'venus', isRocky: true},
        {name: 'terre', isRocky: true},
        {name: 'mars', isRocky: true},
        {name: 'jupiter', isRocky: false},
        {name: 'saturne', isRocky: false},
        {name: 'uranus', isRocky: false},
        {name: 'neptune', isRocky: false}
    ];

    findPlanets():Array<Planet> {
        return this.planets;
    }
}
planetsModule.service('PlanetsService', PlanetsService);

Pour injecter ce service dans le controller, il suffit d’utiliser la variable statique $inject :

class PlanetsController {

    filter:string = null;

    static $inject = ['PlanetsService'];
    constructor(public planetsService) {
    }

    get planetsFiltered() {
        var planets = this.planetsService.findPlanets();
        if (this.filter) {
            return planets.filter((planet) => planet.name.indexOf(this.filter) >= 0)
        }
        return planets;
    }
}

L’injection de dépendance peut se faire de la même façon dans le service, en utilisant la variable statique $inject.

Les tests

Maintenant que les services et les controllers sont des classes, il devient beaucoup plus simple de les tester unitairement, car on peut presque se passer de référence à Angular.

On pourra par exemple conserver l’utilisation du couple Karma/Jasmine proposé par la documentation d’Angular, auquel on ajoutera uniquement le préprocesseur TypeScript.

Les tests deviennent alors :

 

/// <reference path="../definition/jasmine/jasmine.d.ts" />
/// <reference path="../../main/typescript/PlanetsModule.ts" />
describe('PlanetsModule', function () {
    
    describe('PlanetsService', function () {
        var service:PlanetsService;
        
        beforeEach(() => service = new PlanetsService());
        describe('findPlanets', function() {
          it('should give the planet list', function () {
            var findPlanets = service.findPlanets();
            expect(findPlanets.length).toBe(8)
          });
        });
    });

    describe('PlanetsController', function () {
        describe('planetsFiltered', function() {
          it('should give the planet list if no filter', function () {
            var mockPlanet = {name: 'mock'};
            var service = {findPlanets: () => [
                mockPlanet
            ]};
            var controller:PlanetsController = new PlanetsController(service);
            controller.filter = null;
            var planets = controller.planetsFiltered;
            expect(planets).toEqual([mockPlanet])
          });

          it('should give the planet list filtered if filter', function () {
            var mockPlanet = {name: 'mock'};
            var anotherMockPlanet = {name: 'anotherMock'};
            var service = {findPlanets: () => [
                mockPlanet,
                anotherMockPlanet
            ]};
            var controller:PlanetsController = new PlanetsController(service);
            controller.filter = 'ano';
            var planets = controller.planetsFiltered;
            expect(planets).toEqual([anotherMockPlanet])
          });
        });
     });
});

Le défaut de cette approche dans les tests reste la lenteur de compilation. Sur les tests ci-dessus, il faut entre 2 et 3 secondes pour que le code compile et s’exécute.

Les directives

La syntaxe des directives ne peut malheureusement pas facilement être passée sous forme de classe ; en effet le seul constructeur de directive disponible prend une factory en paramètre. Cependant, il est possible de typer fortement le scope, ce qui reste un avantage dans l’écriture d’une directive.

Nous allons créer une directive permettant de colorer en bleu les planètes telluriques, et en rouge les autres

interface PlanetColorScope extends ng.IScope {
    planet:Planet
}

var planetColorDirectiveFactory = function ():ng.IDirective {
    return {
        restrict: 'A',
        scope: {
            planet: '=planetColor'
        },
        link: function (scope:PlanetColorScope, element:ng.IAugmentedJQuery) {
            var color = scope.planet.isRocky ? 'blue' : 'red';
            element.css('color', color);
        }
    }
};

planetsModule.directive('planetColor', planetColorDirectiveFactory);

et son utilisation :

<ul>
    <li ng-repeat="planet in planetsController.planetsFiltered" planet-color="planet">{{planet.name}}</li>
</ul>

Conclusion

On constate d’une manière générale un grand gain en lisibilité lorsque l’on utilise le couple TypeScript et Angular, que ce soit par l’utilisation de classe ou par le typage. De plus la migration de JavaScript vers TypeScript peut se faire en douceur car les syntaxes sont très proches. Le seul gros défaut reste encore le temps de compilation, qui peut facilement atteindre quelques secondes.

Pour aller plus loin, on peut envisager de ne plus utiliser Angular que comme tuyauterie entre les classes et d’utiliser le système de module de TypeScript. Les possibilités sont nombreuses, mais l’objectif de donner plus de structure et de lisibilité à de grosses applications Angular est atteint.

Si vous souhaitez découvrir plus avant TypeScript, vous pouvez vous inscrire au TechEvents du 17 Mars.

Publié par

Publié par Benoît Lemoine

Développeur et fier de l'être, Benoit s'intéresse de près à tout ce qui peut permettre de créer une application web, du HTML aux sources de données, en passant par le javascript et les framework haute productivité. twitter : @benoit_lemoine

Commentaire

5 réponses pour " Angular et TypeScript, un mariage heureux ! "

  1. Publié par , Il y a 5 années

    Angular est devenu en peu de temps LE framework JavaScript du moment. Bénéficiant d’un « effet waouh » impressionnant, il n’en reste pas moins que ni le framework

  2. Publié par , Il y a 5 années

    Une fois qu’on a typé les controllers, il reste toujours les vues HTML qui empechent le refactoring rapide (qui pour moi est LE gros avantage du typage statique).

    Par exemple, mettons que je renomme isRocky par isTelluric, la compilation va échouer pour planetColorDirectiveFactory.link et on est safe.

    Par contre, si je renomme name, la compilation ne va pas échouer pour le template et on introduit un bug…

    Existe-t-il une solution / idée pour contrer ce point ?

  3. Publié par , Il y a 5 années

    Malheureusement, je pense qu’il n’y a pas d’autres solutions que de faire des tests end-to-end pour ce type de problème.
    En tout cas, il n’existe pas aujourd’hui à ma connaissance de compilateur de template angular qui vérifie dans le controller que la propriété existe effectivement.

  4. Publié par , Il y a 5 années

    OK merci pour l’info.

    Peut être quelqu’un explorera cette piste un jour…

  5. Publié par , Il y a 4 années

    Nous utilisons Typescript et AngularJS sur un de nos projets, la solution que j’ai mis en œuvre pour limiter les possibilités d’erreur lors du refactoring est la suivante :
    – Pour chaque contrôleur, le développeur DOIS créer une interface, le contrôleur implémentant bien sûr cette interface. Cette interface ne dois JAMAIS être modifiée sans validation approfondie de tous les templates.
    – Les designers d’IHM ne doivent utiliser QUE ce qui est mis à disposition par les interfaces.

    Ce n’est pas magique, mais ça permet quand même de limiter les impactes d’un refactoring « sauvage »

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.