TypeScript
Introduction
1. Objectifs
JavaScript est particulièrement malléable comme nous avons pu le voir, mais il nécessite un effort de programmation pour obtenir des fonctionnalités de base (gestion de l’héritage, de la surcharge, etc.). Ces fonctionnalités sont courantes dans les langages actuels comme Java, C#, Python, etc. et il peut sembler regrettable que JavaScript (ECMAScript 5) n’ait pas évolué au même rythme. La société Microsoft a mis au point un sur-ensemble de JavaScript appelé TypeScript. Quand on parle de sur-ensemble cela signifie que JavaScript est inclus dans TypeScript qui ajoute ce qui semble manquer au langage. Un de ses avantages est l’étape de compilation de TypeScript qui consiste à obtenir un code JavaScript compatible avec les navigateurs.
Les dernières versions de JavaScript (à partir d’ECMAScript 2015) reprennent ces évolutions. Elles devraient donc se généraliser au fur et à mesure des mises à jour des moteurs JavaScript au sein des navigateurs.
2. Hello world
Nous allons partir de la forme la plus simple en JavaScript pour réaliser notre premier exemple. L’idée est d’obtenir le compilateur TypeScript via le gestionnaire de package de Node.js (NPM).
Si vous ne disposez pas de l’utilitaire NPM, vous pouvez le télécharger sur le site de nodejs.org...
Variable et constante
1. Variable
Nous avons jusqu’ici déclaré nos variables avec le mot-clé var afin de limiter la portée à la fonction courante. Contrairement aux langages courants, il n’existait pas de portée intermédiaire entre l’espace global et l’espace de la fonction. TypeScript propose le mot-clé let qui s’utilise comme var à la différence que la portée de la variable est limitée au bloc courant et non à la fonction dans sa globalité.
exemple2.ts
function table( max ) {
let i = 1;
for ( ;i<=max;i++ ) {
for ( let j = 1;j<=max;j++ ) {
console.log( i + "*" + j + "=" + ( i * j ) );
}
}
}
table(7);
Le compilateur tsc construit au final le fichier suivant :
function table(max) {
var i = 1;
for (; i <= max; i++) {
for (var j = 1; j <= max; j++) {
console.log(i + "*" + j + "=" + (i * j));
}
} ...
Typage
1. Déclaration
a. Variable
Lors de la déclaration d’une variable, constante ou bien d’un paramètre, il suffit de déclarer le type après le nom.
function table(max:number) {
for ( let i:number=0; i<=max; i++ ) {
for ( let j:number=0; j <=max; j++ ) {
let message:string = `${i}*${j}=${i*j}`;
console.log( message );
}
}
}
table(8);
Nous avons dans cet exemple déclaré un paramètre max et deux variables i,j comme étant des nombres. Une variable message est une chaîne.
Si le typage n’est pas respecté à la compilation, par exemple avec table("8"); le compilateur nous l’indique avec :
error TS2345: Argument of type '"8"' is not assignable to parameter
of type 'number'.
b. Fonctions
Le type retourné par une fonction est défini après la déclaration. Si la fonction ne retourne pas de valeur, elle est alors de type void.
function max(...params:number[]):number {
let m:number=0;
for (let i:number=0; i < params.length; i++ ) {
m = Math.max(params[i],m);
}
return m;
}
alert( "Max =" + max(3,2,6));
Nous avons ici une fonction max qui retourne un nombre. En argument, nous avons un nombre de paramètres variable grâce aux trois points. Pour récupérer ensuite les valeurs, on passe par un type "tableau de nombre" pour ce cas.
2. Chaînes
Le type string désigne la chaîne. Il existe deux formats de chaîne :
-
Celle entre guillemets comme nous avons l’habitude....
Classes
1. Déclaration et usage
Jusqu’ici, nous avons vu qu’une classe était en réalité réduite à une fonction « constructeur » et que l’objet prototype servait à mettre en commun les méthodes de notre classe. Cela fonctionne mais ce n’est pas très naturel pour un développeur Objet. TypeScript introduit à cet effet, comme avec la dernière version de JavaScript, un mot-clé class.
Dans l’exemple ci-dessous, nous avons construit une classe représentant une personne avec des champs nom et prenom. Le constructeur est explicitement nommé constructor.
class Personne {
nom:string;
prenom:string;
constructor(nom:string,prenom:string) {
this.nom = nom;
this.prenom = prenom;
}
bonjour() {
alert( "Bonjour " + this.prenom + " " + this.nom );
}
}
let personne:Personne = new Personne( "Brillant", "Alexandre" );
personne.bonjour();
La création d’une instance se fait « classiquement » avec l’opérateur new.
À noter que l’accès au champ doit se faire obligatoirement via le mot-clé this alors qu’il est généralement implicite dans les langages objets.
2. Héritage
L’héritage est aussi naturellement obtenu par le mot-clé extends.
class Employe extends Personne {
job:string;
constructor(nom:string,prenom:string,job:string) {
super(nom,prenom);
this.job = job;
}
bonjour() {
super.bonjour();
alert( "Vous êtes " + this.job );
}
}
let personne:Employe = new Employe( "Brillant", "Alexandre",
"Auteur" );
personne.bonjour();
Nous avons ici hérité de la classe Personne. Grâce au mot-clé super, nous avons la possibilité...
Interfaces
1. Déclaration
L’interface permet de créer des types supplémentaires dédiés aux objets et fonctions. Cela va servir à contrôler que les objets utilisés ont bien certaines caractéristiques.
Par exemple, nous voulons un type AvecNom désignant les objets ayant un champ nom ce qui est le cas de notre classe Personne. Elle peut avoir d’autres champs, ce n’est pas important tant qu’elle a au moins le champ nom.
interface AvecNom {
nom:string;
}
class Personne {
nom:string;
prenom:string;
public constructor(nom:string,prenom:string) {
this.nom = nom;
this.prenom = prenom;
}
}
class Produit {
nom:string;
public constructor(nom:string) {
this.nom = nom;
}
}
let personne:AvecNom = new Personne( "brillant", "alexandre" );
let produit:AvecNom = new Produit( "hal" );
alert( produit.nom );
Nos variables personne et produit, bien que représentant deux classes Personne et Produit indépendantes, sont typées de la même manière avec l’interface AvecNom.
2. Propriétés optionnelles et en lecture seule
Il arrive que certaines propriétés ne soient pas encore définies. Dans ce cas, il est possible d’indiquer avec un point d’interrogation dans l’interface qu’une propriété peut être manquante et éviter ainsi une erreur de compilation.
Nous pourrions modifier notre interface AvecNom ainsi :
interface AvecNom {
nom:string;
prenom?:string;
}
Dans ce cas, le code précédent continue à fonctionner car l’instance personne possède bien le membre prenom et l’instance produit possède bien le membre prenom. Le compilateur en revanche autorisera l’accès à la propriété prenom même sur le produit.
alert( produit.prenom ); // OK
Si nous invoquons un autre membre que nom et prenom, nous aurons un message d’erreur...
Génériques
1. Déclaration et usage
Jusqu’ici, nous avons employé des fonctions ou des classes avec des paramètres ou des membres ayant un type bien précis. Cela signifie que nous ne pouvons pas réutiliser ces fonctions et classes en dehors de ces types. Or, il existe des cas où nous voudrions pouvoir manipuler nos fonctions/classes avec des types différents. C’est ce que nous proposent de faire les « génériques ».
function tab<T>(a:T,b:T):T[] {
let t = new Array<T>();
t.push( a );
t.push( b );
return t;
}
alert( tab<string>( "hello", "world" ) );
alert( tab<number>( 10, 20 ) );
alert( tab<boolean>( true, "false" ) ); // ERREUR
Dans notre exemple, nous stockons deux arguments dans un tableau. Pour que cela fonctionne avec n’importe quel type, nous ajoutons au nom de la fonction tab un paramètre T entre chevrons correspondant au type de nos arguments et de notre tableau résultat. Lors de l’invocation de notre fonction, le paramètre T est remplacé par le type souhaité. Nous avons alors, dans le premier cas, un tableau de chaînes, dans le deuxième cas, un tableau de nombres. Le dernier cas provoque une erreur de compilation puisque nos arguments doivent être des booléens et que nous avons passé une chaîne en deuxième argument.
Il est possible de laisser le compilateur trouver lui-même le type attaché à T avec l’inférence de type. Par exemple, si nous écrivons :
tab(...
Modules
1. Déclaration et usage
Pour concevoir un module, il suffit d’avoir le mot-clé export ou import dans un fichier TypeScript. Pour rendre disponible une variable, fonction, interface ou classe dans un module, on la préfixe par export.
Prenons l’exemple de cette classe présente dans le fichier personne.ts :
class Personne {
private nom : string;
private prenom : string;
public constructor( nom : string, prenom : string ) {
this.nom = nom;
this.prenom = prenom;
}
public hello():void {
alert( "Hello " + this.nom + " " + this.prenom );
}
}
Le compilateur l’a traduite en JavaScript par :
var Personne = (function () {
function Personne(nom, prenom) {
this.nom = nom;
this.prenom = prenom;
}
Personne.prototype.hello = function () {
alert("Hello " + this.nom + " " + this.prenom);
};
return...
Espace de noms
1. Déclaration et usage
L’espace de noms avec le mot-clé namespace va héberger toutes les variables, fonctions, interfaces, classes ayant un regroupement logique. Les déclarations dans l’espace de noms vont rester locales à l’espace de noms. Pour que les déclarations soient rendues disponibles hors de l’espace de noms, il faut les préfixer par le mot-clé export. Pour l’usage, on préfixe un élément exporté par l’espace de noms.
personnes.ts :
namespace Personnes {
let message = "Hello";
interface PeutDireBonjour {
hello();
}
export class Personne implements PeutDireBonjour {
private nom : string;
private prenom : string;
public constructor( nom : string, prenom : string ) {
this.nom = nom;
this.prenom = prenom;
}
public hello():void {
alert( message + this.nom + " " + this.prenom );
}
}
export class Employe extends Personne {
public constructor( nom : string, prenom : string ) {
super( nom, prenom );
}
}
}
main.ts ...