Il y a 9 ans -
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.
Commentaire
5 réponses pour " Angular et TypeScript, un mariage heureux ! "
Published by Nicolas , Il y a 9 ans
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
Published by chris , Il y a 9 ans
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 ?
Published by Benoît Lemoine , Il y a 9 ans
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.
Published by chris , Il y a 9 ans
OK merci pour l’info.
Peut être quelqu’un explorera cette piste un jour…
Published by Grégory DELPU , Il y a 7 ans
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 »