Logo Spiria

AngularJS + TypeScript

26 septembre 2016.
Angular + TypeScript.

Il y a quelque temps déjà qu’on entend parler de TypeScript, et je me suis retenu de m’y lancer pendant tout ce temps, car, comme probablement plusieurs d’entre vous, je me disais qu’un langage qui tente d’en générer un autre ne peut être que désastreux. Récemment, chez Spiria, nous avons entamé un projet sur lequel nous désirions utiliser Angular (AngularJS 1 classique) comme front-end et, puisque beaucoup de membres de l’équipe ont un bagage .NET, nous étions curieux de voir s’il était envisageable d’utiliser TypeScript pour monter notre application Angular. J’ai ainsi été chargé de faire cette recherche de quelques jours et je vous présente donc mes résultats (indice : ils sont positifs).

Pour ceux plus familiers avec l’univers JavaScript, soyez rassuré qu’un projet TypeScript fonctionnera sans problème avec vos outils de tests et d’automatisation favoris (dans mon cas, Gulp et Karma/Jasmine), et il vous est même possible d’automatiser la compilation de votre TypeScript en JavaScript au fur et à mesure que vous sauvegardez vos fichiers !

TypeScript

Ressources principales

Ces ressources du site officiel de TypeScript sont amplement suffisantes pour vous donner une bonne idée de ce que TypeScript peut faire. Je ne répéterai pas inutilement ce qui est déjà parfaitement couvert à cet endroit et vous invite plutôt à en faire une bonne lecture et de jouer un peu avec du TypeScript. Je ferai également plusieurs références au long de ce texte à des aspects de TypeScript que vous trouverez dans le Handbook.

TypeScript, c’est du JavaScript

La règle la plus importante à se souvenir : du TypeScript sera ultimement toujours du JavaScript. Si vous avez des outils qui s’appliquent sur des fichiers JavaScript (Gulp, Grunt, Jasmine, etc.) et bien rien n’est perdu, ils seront toujours 100 % utiles, et probablement que vous n’aurez rien à changer avec votre processus de déploiement ou de test, mis à part l’ajout d’une étape de compilation de votre TypeScript en JavaScript au début de tout ça.

La deuxième règle importante à se souvenir : du code JavaScript valide est du TypeScript valide. Vous pouvez voir TypeScript comme un “superset” de JavaScript qui contient une syntaxe étendue pour, entre autres, mieux gérer les concepts orientés objet et la modularisation de votre code.

Finalement, la troisième règle importante : une erreur de syntaxe TypeScript ne signifie pas nécessairement une erreur JavaScript. Ultimement, TypeScript compile en JavaScript, et même si notre fonction TypeScript s’attend à une string, on peut quand même lui passer un integer, et JavaScript va simplement appliquer sa fameuse conversion automatique :

function newAlert(msg: string) {
    alert(msg);
}
newAlert(4); //Erreur TypeScript, mais puisque JavaScript appliquera sa fameuse conversion automatique, il alertera tout de même "4" une fois compilé.

Bref, ça nous permet de tricher. Le but de TypeScript est cependant d’arrêter de tricher, et de s’assurer que les objets sont utilisés comme il se doit et que les contrats sont respectés, alors nous allons éviter le plus possible de tricher. Je mentionne ces règles dès le début afin de bien exprimer que TypeScript ne tente pas de faire du code à votre place, mais vient simplement vous aider à mieux le structurer.

Pour la suite, voyons comment appliquer ce paradigme avec Angular.

AngularJS

Non, on ne parle pas de Angular 2, mais bien du classique (AngularJS 1). Pourquoi ? Parce que Angular classique a passé l’épreuve du temps et que, même si Angular 2 a été monté avec TypeScript en tête, la comparaison sera plus frappante en utilisant quelque chose qui est déjà connu.

Ressources principales

Un développeur expérimenté en Angular et/ou NodeJS et/ou ECMA2015 sera sans doute très familier avec ce qu’il trouvera dans le code l’application ToDoMVC. Cependant, si vous êtes débutant, il serait injuste de vous laisser explorer le code et vous laisser deviner ce qui s’y passe. Je vous accompagnerai donc à travers cette application pour vous expliquer du mieux que je peux ce que TypeScript fait en arrière-plan afin de générer une application Angular en JavaScript qui fonctionne.

Tout d’abord, dirigez-vous vers le fichier Application.ts. Il y a également le fichier Application.js qui est la concaténation de tous les fichiers TypeScript compilés. C’est le résultat d’une commande tsc avec un paramètre -out. Vous pouvez vous y reporter pour tenter de mieux comprendre comment TypeScript compile en JavaScript, mais nous allons y revenir plus tard, pour l’instant concentrons-nous sur Application.ts.

Vous verrez ce code :

/// 
module todos {
    'use strict';
 
    var todomvc = angular.module('todomvc', [])
            .controller('todoCtrl', TodoCtrl)
            .directive('todoBlur', todoBlur)
            .directive('todoFocus', todoFocus)
            .directive('todoEscape', todoEscape)
            .service('todoStorage', TodoStorage);
}

D’abord, le commentaire :

///

est tout simplement une instruction au compilateur TypeScript pour inclure du code qui se situe dans un autre fichier, soit le fichier _all.ts. Allez voir ce fichier et vous verrez que l’auteur inclut d’autres fichiers, dont des librairies .d.ts et les fichiers .ts qu’il a écrit lui-même *.

(*À noter que je ne suis pas entièrement à l’aise avec la méthode de référence des fichiers TypeScript suggérée ici. Manuellement, il s’agit d’un fardeau de plus. Rassurez-vous, il y a des alternatives à cette méthode, aidées d’un peu d’automatisation, qui nous évitent bien des soucis !)

Parlons-en, de ces fichiers .d.ts :

///

Comme vous vous en doutez, il s’agit d’une référence à la librairie Angular, en mode TypeScript. Si vous allez voir ce fichier, vous verrez qu’il n’y a que des déclarations d’interfaces qui couvrent la totalité des fonctions publiques Angular. Ces interfaces permettent d’utiliser les fonctions d’Angular dans un environnement TypeScript, ce qui ne fait que renforcer les types des objets et paramètres sans avoir à retaper la librairie au complet. Ultimement, ce fichier .d.ts est optionnel, mais votre compilateur TypeScript risque de vous donner plein d’erreurs d’utilisation d’objets non déclarés et il faudra vous assurer vous-mêmes de bien utiliser les méthodes. Bref, vous devrez utiliser Angular comme si vous faisiez du JavaScript pur. Comme les internets sont un endroit fantastique, il y a déjà une place où des centaines de librairies populaires JavaScript ont leurs équivalents en .d.ts : DefinitelyTyped.

Ensuite, strictement :

'use strict';

est simplement une déclaration qui notifie que des fautes de syntaxe, qui sont traditionnellement acceptées en JavaScript, retournent plutôt des erreurs. Plus de détails ici.

Puis, les modules :

module todos { ... }

Un module est, pour simplifier, un namespace. JavaScript étant ce qu’il est, un module est aussi un objet et en JavaScript traditionnel, on déclare un “module” de cette manière :

var todos;
(function (todos) {
    ...
})(todos || (todos = {}));

Bref, si ce bout de code vous donne un mal de tête, dites-vous que c’est exactement ce que TypeScript essaie de régler. Pour plus de détails sur le patron modulaire JavaScript, visitez ce lien.

En gros, dans ce module, on définit des fonctions, des interfaces, des classes et des propriétés (puisque notre module est également un objet) que l’on peut rendre publiques avec le terme export devant la déclaration. Il est également possible de déclarer le même module à plusieurs endroits différents, et même dans plusieurs fichiers différents, et (tant que les autres fichiers sont référencés tel que vu plus haut) tout ce qui a été déclaré dans ce module est partagé à travers toutes ses déclarations.

module Foo {
    export function bar() {
        alert('Hello World!');
    }
}
module Foo {
    bar(); //alerte 'Hello World!'
}

Sans aller trop dans le détail, le bout le plus intéressant des modules, c’est l’importation d’un module dans un autre (ou n’importe où, vraiment) :

module Foo {
    export function bar() {
        alert("Hello World!");
    }
}
module Main {
    /// 
    import foo = Foo; //Optionnel, mais on évite à faire du refactoring plus tard si on advient à changer le nom du module Foo. C’est une bonne pratique à avoir.
    foo.bar();
}

Enfin, le code

Le reste du code, si vous êtes le moindrement familier avec Angular — et vous l’êtes puisque vous avez au moins pris la peine de suivre le tutoriel Angular avant d’entamer cette lecture —, ressemble pratiquement à du JavaScript pur. On déclare notre module Angular auquel on attache nos contrôleurs, directives et services exactement comme on le ferait en JavaScript.

Les fonctions TodoCtrltodoBlurtodoFocustodoEscape et TodoStorage sont déclarées dans les autres fichiers qu’on a référencés et, puisqu’elles ont été déclarées dans le même module todos, elles sont donc disponibles. Par contre, il y a une surprise qui vous attend si vous allez voir ces fonctions : elles ne sont pas des fonctions, mais bien des classes !

Ça prend de la classe…

En TypeScript, il y a les classes que l’on peut utiliser qui remplacent les “fonctions-classes” de JavaScript. Pour les habitués de langages fortement typés à la C# ou Java, vous serez en terrain connu. Je n’embarquerai pas tous les détails et les subtilités de la manière de déclarer et utiliser des classes en JavaScript, mais voici en gros comment on peut le faire :

function Person() {
    //Méthodes non-héritées ici
}
 
Person.prototype = {
    //Méthodes héritées ici
    setName: function (name) {
        this.name = name;
    }
}
 
function TalkingPerson() {
}

//__proto__ est un peu la définition de la structure de l’objet. Appliquer un changement au __proto__ se reflète dans tous les objets instanciés via 'new TalkingPerson()'
//ici, on dit donc au moteur JavaScript que toutes les méthodes du prototype de Person seront aussi accessibles au prototype de TalkingPerson
TalkingPerson.prototype.__proto__ = Person.prototype;
TalkingPerson.prototype.sayName = function () {
    alert('My name is ' + this.name);
}
 
var jane = new TalkingPerson();
jane.setName('Jane');
jane.sayName(); //alerte 'My name is Jane'
 
var john = new Person();
john.setName("John");
john.sayName(); //méthode 'sayName' non définie

Ça ne semble peut-être pas si compliqué, mais ça le devient très rapidement. Simplement la gestion du scope (la fameuse variable this) devient rapidement un cauchemar lorsqu’un joue avec le prototype d’objets. Je vous invite à jouer avec ce bout de code, à ajouter des propriétés dans les prototypes, des méthodes, à voir ce qui est accessible à un objet instancié, ce qui est partagé avec les autres objets, etc. et vous comprendrez assez vite l’attrait de TypeScript.

En revanche, voici le même bout de code en TypeScript :

class Person {
    name: string;
    setName(name) {
        this.name = name;
    }
}
class TalkingPerson extends Person {
    sayName() {
        alert(this.name);
    }
}
var jane = new TalkingPerson();
jane.setName("Jane");
jane.sayName();
var john = new Person();
john.setName("John");
john.sayName();

Beaucoup plus simple et intuitif, n’est-ce pas ?

Pour en revenir à Angular

Donc, traditionnellement, on aurait défini notre objet ToDoCtrl à travers une fonction et assigné une propriété $inject pour laisser Angular faire son injection de dépendance. Ensuite, nous aurions défini les propriétés et méthodes disponibles de notre contrôleur, possiblement quelques fonctions privées, un peu comme suit : 

TodoCtrl.$inject = ['dependency1', 'dependency2'];
function TodoCtrl(dependency1, dependency2) {
    // 'vm' veut dire 'view model'. On utilise ce style pour pouvoir utiliser la fonctionnalité 'controller as' d’Angular. Voir le guide de style de JohnPapa en lien plus haut dans l’article.
    var vm= this;
 
    vm.foo = 1;
    vm.one = dependency1.getOne();
    vm.two = dependency2.getTwo();
    vm.bar = bar;
  
    function bar(){
        vm.foo++;
    }
}

En TypeScript, on le déclare plutôt ainsi :

class TodoCtrl {
 
    public foo: integer;
    public one: IOne; //l’interface devra être définie à priori par vous, ou, s’il s’agit d'une librairie externe, extraite de son fichier .d.ts
    public two: ITwo;
  
    public static $inject = ['dependency1', 'dependency2']; //public car Angular a besoin de le voir et statique car l’injecteur doit faire son travail avant même d’instancier la classe.
  
    constructor(private dependency1: IDependency1;, private dependency2: IDependency2){
        this.foo = 1;
        this.one = dependency1.getOne();
        this.two = dependency2.getTwo();
    }
  
    bar(){
        this.foo++;
    }
}

Un peu plus verbeux, mais amplement plus lisible. Le simple fait de pouvoir déclarer une propriété ou une méthode publique indique clairement que nous pourrons faire notre binding avec, tandis que celles déclarées privées sont clairement indiquées et ne seront pas accessibles de l’extérieur.

Donc

Dans le fichier Application.ts, les contrôleurs, directives et services rattachés à notre application Angular (lignes 12 à 16) sont donc des définitions de classes. Angular, au moment de générer l’application, utilisera ces définitions de classes pour instancier les instances qu’il lui faudra afin de donner vie à votre application. Vous pouvez explorer le fichier Application.js et, où nous en sommes rendu, vous devriez être en mesure de comprendre ce qui s’y passe en le comparant avec les fichiers .ts de nos contrôleurs, services et directives.

Finalement

Je vous invite à suivre les instructions trouvées dans le ReadMe de l’excellente démo de TodoMVC afin de comprendre comment déployer le projet sur votre machine et compiler le TypeScript via une commande npm. Pour ceux qui ne sont pas du tout familiers avec Node.js et npm, voici un petit résumé de ce qui se passe :

  • NodeJS est un langage de script que vous pouvez installer sur votre machine et qui, un peu comme PHP, Python ou Ruby, permet d’exécuter des scripts sur votre machine (le serveur). NodeJS est, comme son nom l’indique, du JavaScript pur.
  • npm est le Node Package Manager. Il est similaire à gem pour Ruby, pip pour Python, composer pour PHP ou même nuget pour .NET.
  • Lorsqu’on instancie un projet npm via package.json, on crée un fichier package.json qui contient la liste de toutes les dépendances à installer pour que l’application fonctionne.
  • Ce fichier package.json, qu’on peut voir dans l’application TodoMVC, peut contenir également des scripts, qui sont des commandes que l’on peut exécuter en appelant npm nom-de-script (dans le cas de TodoMVC, npm compile exécutera la commande tsc --sourcemap --out js/Application.js js/_all.ts).
  • tsc est la commande qui compile le TypeScript en JavaScript. --sourcemap permet de faire du sourcemapping. --out js/Application.js donne l’instruction de concaténer le résultat de la compilation dans un seul fichier. Finalement, le dernier paramètre js/_all.ts indique le fichier à compiler. Dans ce cas, le fichier est vide, mais puisqu’il met en référence les autres fichiers, le compilateur va également chercher ces fichiers afin de les compiler.
  • Et puis, dans index.html, on peut voir la référence vers le fichier compilé, soit Application.js, en plus des librairies JavaScript nécessaires, dont Angular. À noter que les librairies incluses sont bien toujours en JavaScript, et le fait d’inclure un fichier .d.ts dans votre TypeScript ne signifie pas que la librairie JavaScript sera incluse dans votre code !

Prochaine lecture

Dans un autre billet, j’expliquerai comment j’ai ensuite exploré une manière d’automatiser le build de mon application (soit la compilation de mon TypeScript en JavaScript) de manière automatique avec Gulp. J’expliquerai également comment j’ai exploré Jasmine pour mes tests unitaires et Karma pour l’exécution en continu de ces tests. Toutes ces dépendances seront gérées via npm, qui est pratiquement devenu un incontournable dans la communauté JavaScript.