L’essentiel de la syntaxe Typescript en 10 min

Le but de cet article est de présenter de façon succinte la syntaxe de Typescript. Pour un développeur C#, l’apprentissage de Typescript n’est pas forcément aisé car beaucoup de mots clé se ressemblent et peuvent préter à confusion. On peut facilement tomber dans le piège de croire que Typescript est comme C#. Tout deux étant des langages procéduraux, les notions sont évidemment similaires, toutefois de nombreuses subtilités poussent à rester vigilant et à vérifier tous les détails de syntaxe.

Le code Typescript n’est pas directement exécuté, il est compilé en Javascript. C’est le code Javascript qui sera exécuté par les browsers. Javascript étant exécutable par tous les browsers, le choix de Typescript comme langage de programmation est pertinent puisqu’il sera, de fait, facilement exécutable.

Un autre gros avantage de Typescript par rapport à Javascript est de pouvoir typer fortement les variables ce qui permet de rendre ce langage moins sensible aux bugs révélés au runtime. En effet, de nombreuses erreurs peuvent être décelées directement à la compilation.

D’autres apports syntaxiques sont faits par Typescript comme les classes, les modules ou les interfaces ce qui permet de rendre le langage plus attrayant pour des développeurs habitués à ces notions en C# ou Java.

Typescript étant lié à Javascript, on va, en préambule, donner quelques indications concernant ECMAScript. Ensuite, quelques explications seront données sur les types les plus couramment utilisés, les fonctions, les classes et les interfaces. Enfin, on va décrire quelques instructions courantes.
Volontairement, cet article ne traite pas des exports de classes, des modules, des références et de l’utilisation de bibliothèques tierces. Ces sujets feront l’objet d’un autre article.

Sommaire

ECMAScript

Typage et variables
Typage implicite
  Chaine de caractères sur plusieurs lignes
Types primitifs
  Chaine de caractères avec des expressions incluses
any
object
undefined
null
void
enum
Tuple
Effectuer un “cast” pour changer de type
Différences entre “let” et “var”
Object literal
  Déstructuration
  Déstructuration avec reste
Array
  Déstructuration d’un tableau
  Déstructuration d’un tableau avec reste

Fonctions
Arrow functions
Définir une fonction dans un “object literal”
Paramètre optionel
Utiliser un “object literal”
const

Classe
Instanciation
  Instanciation d’une classe en utilisant “typeof”
Notation abrégée des membres dans le constructeur
Fonctions
Accesseurs
readonly
Propriété statique
  Atteindre une propriété statique avec “typeof”
Héritage
  “Overrider” une méthode
Effectuer un “cast”
Classe abstraite
Generic

Combinaisons de types
Intersection
Union
Utilisation de “type guards”
  is
  typeof
  instanceof

Interface
Définir un objet quelconque satisfaisant une interface
Utilisation des interfaces avec des “object literals”
  Propriété optionelle
  readonly
Utilisation des interfaces avec des classes
Héritage entre interfaces
Déclarer une signature de fonction sous forme d’interface
  Déclarer une fonction d’indexation sous forme d’interface
Interface héritant d’une classe

Instructions courantes
if…then…else
  Opérateurs de comparaison
  Opérateurs logiques
  Comportement avec “null” et “undefined”
  Différences entre “==” et “===”
Boucles
  Boucle “for”
  Boucle “while”
  Boucle “do…while”
  break/continue
  Boucle infinie
  for…of
  for…in
try…catch

Pour conclure…

ECMAScript

ECMAScript est un ensemble de normes édictées par l’ECMA (i.e. European Computer Manufacturers Association) pour standardiser des langages de type script. L’ECMA-262 est le standard publié par l’ECMA concernant les langages scriptés.

Javascript est une implémentation de ce standard, ce qui signifie qu’il implémente une bonne partie des spécifications mais pas toutes. En plus de la conformité de Javascript avec le standard ECMAScript, il faut prendre en compte la compatibilité des browsers. Même si une version de Javascript est compatible avec une version d’ECMAScript, ça ne veut pas dire que tous les browsers supportent toutes les spécifications. Certains sites référencent les spécifications supportées en fonction du browser, par exemple:
http://kangax.github.io/compat-table/es6/.

Les versions actuellement les plus connues d’ECMAScript sont les suivantes:

  • ES5 (pour ECMAScript 5e version): publiée en 2009, elle est la version la plus supportée par les browsers. En paramétrant cette version comme version cible à la compilation, on est sûr de pouvoir être compatible avec la plupart des browsers.
  • ES6/ES2015 (pour ECMAScript 6e version): publiée en 2015 et renommée ES2015. Au fur et à mesure, les browsers se rendent compatibles avec l’ensemble des fonctionnalités du standard. Une technique consiste à utiliser des éléments de syntaxe ES6/ES2015 pour bénéficier des avantages de cette version et à compiler ces éléments en ES5 pour que le code soit exécutable par la plupart des browsers.

D’une façon générale, il est possible de tester la syntaxe Typescript sur le site: typescriptlang.org

Typage et variables

Tous les types primitifs Javascript sont utilisables en Typescript. Comme indiqué plus haut, Typescript est fortement typé, ce qui sous-entend que le compilateur effectue des vérifications dans la cohérence des types utilisés à partir de la définition d’une variable.

En Typescript, il est toutefois possible d’utiliser des types moins précis comme undefined ou any comme en Javascript.

On peut déclarer une variable sans l’initialiser en utilisant la syntaxe générale:

let <nom de la variable>: <type de la variable>  

ou

var <nom de la variable>: <type de la variable>  

Une variable peut être définie et initialisée de cette façon:

let <nom de la variable>: <type de la variable> = <valeur d'initialisation> 

ou

var <nom de la variable>: <type de la variable> = <valeur d'initialisation> 

Par exemple:

let varNum1: number = 2; 
var varNum2: number = 3; 
let simpleString: string = 'chaine de caractères'; 
var otherSimpleString: string = 'autre chaine de caractères'; 

var n’a pas la même signification qu’en C#. On peut le voir dans les exemples précédents, var ne prive de préciser le type d’une variable comme c’est le cas en C#.

Typage implicite

Le type peut être déterminé de façon implicite à partir de la valeur d’initialisation (i.e. type inference). On peut, ainsi, éviter de préciser explicitement le type.

Par exemple:

let varNum1 = 2; // le type est indiqué de façon implicite: number
var varNum2 = 3;  
let simpleString = 'chaine de caractères'; // le type est string

Types primitifs

On peut résumer les types primitifs les plus importants provenant du Javascript dans le tableau suivant:

Déclaration Remarques Exemples
Valeur booléenne boolean Dans les premières versions de Typescript (< version 0.9), un booléen se déclarait avec le mot-clé bool. Toutefois dans les versions récentes, le mot-clé utilisé est boolean let booleanVar: boolean = false;
Chaine de caractères string La valeur d’une chaine de caractères peut être déclarée en utilisant des guillemets "..." (comme en C#) ou des simples quotes '...' let stringVar: string = 'chaine de caractères';
Valeur numérique number Tous les nombres sont des valeurs à virgule flottante let numberVar: number = 76;

Certains opérateurs Javascript existent en Typescript. Par exemple l’opérateur !! qui permet de convertir une chaine de caractère en booléen:

let stringValue: string = 'yes';  
let booleanValue: boolean = !!stringValue; 

La valeur 'yes' est transformée en booléen.

Chaine de caractères sur plusieurs lignes

On peut définir des chaines de caractères sur plusieurs lignes en utilisant le caractère ` (` est le caractère accent grave accessible avec [AltGr] + [7]):

let multipleLineString: string = `Ligne 1 
Ligne 2 
Ligne 3`; 

Chaine de caractères avec des expressions incluses

On peut inclure dans une chaine de caractères des expressions qui seront interprétées. Pour utiliser ces expressions, il faut entourer la chaine de caractère par ` (` est le caractère accent grave accessible avec [AltGr] + [7]) et inclure l’expression dans une déclaration ${...}.

Par exemple:

let numberVar: number = 5; 
let stringWithNumber: string = `La valeur + 1 est: ${ number + 1}`; 

any

Ce type provient du Javascript et indique que le type est indéfini. Si une variable est déclarée avec ce type, on pourra effectuer des affectations avec des types différents.

Par exemple:

let anyVariable; // implicitement le type est any 
let otherVariable: any = 67; // le type est explicitement any 
otherVariable = 'chaine de caractères'; // possible car le type est any 

Même si une variable est de type any, il est possible d’appeler des fonctions dépendantes de son type réel:

let anyVariable: any = 34; // le type réel est number 
let expo = anyVariable.toExponential(2); // Possible 

object

Object est proche du même type en C#. Le type Object permet de déclarer une variable de type indéfini. Comme pour any, on peut y affecter des valeurs de types différents.

La différence avec any est qu’on ne peut pas évoquer une fonction dépendant du type réel:

let objectVariable: Object = 34; // le type réel est number 
let expo = objectVariable.toExponential(2); // Pas possible 

undefined

undefined est une valeur que l’on peut affecter à tous les types:

let variable: number = 34; 
variable = undefined; 

Il s’agit aussi d’un type à part entière. On peut donc déclarer une variable de type undefined:

let undefinedVar: undefined = undefined; // On ne peut rien affecter à cette variable à part undefined et null. 

null

Par défaut, null est une valeur possible pour tous les types de variables. On peut donc affecter la valeur null aux variables de ce type:

let nullNumber: number = null;

On peut aussi affecter null à une variable de type undefined.

Comme undefined, il s’agit aussi d’un type à part entière. On peut donc déclarer une variable de type null:

let nullVar: null = null; // On peut affecter seulement null ou undefined. 

void

Il s’agit d’un type pour indiquer “absence de type”. Comme en C#, il permet d’indiquer qu’une méthode n’a pas de valeur en retour.

Si une variable est de type void, on ne peut y affecter que le type null ou undefined:

let voidVar: void = undefined;

enum

Contrairement aux autres types, ce type n’existe pas en Javascript et est un apport du Typescript. Il est proche du type enum en C#, il permet de définir un ensemble de valeurs:

enum Language { 
    Csharp, Typescript, Javascript 
} 

On peut affecter une variable avec une valeur particulière de l’enum:

let choosedLanguage: Language = Language.Typescript; 

Par défaut, la valeur numérique associée à la 1ère valeur de l’enum est 0 et peut être utilisée de cette façon:

let choosedLanguage: string = Language[0];

On peut aussi affecter une valeur numérique particulière aux valeurs de l’enum:

enum Language { 
    Csharp = 1,  
    Typescript,  // implicitement la valeur sera 2 
    Javascript   // implicitement la valeur sera 3 
} 

On peut aussi affecter des valeurs arbitraires:

enum Language { 
    Csharp = 4,  
    Typescript = 7,  
    Javascript = 9 
}

Tuple

La notion est la même qu’en C#, ce type permet de définir des paires ou des triplets de valeurs:

let tupleValue: [boolean, string, number]; // pour déclarer un triplet  

L’initialisation se fait de cette façon:

tupleValue = [false, 'Chaine de caractère', 34]; 

On peut atteindre une valeur particulière en utilisant un index:

let booleanValue: boolean = tupleValue[0]; 
let stringValue: string = tupleValue[1]; 
let numberValue: number = tupleValue[2];

Effectuer un “cast” pour changer de type

Pour changer le type d’une variable, on peut effectuer un cast avec 2 syntaxes:

let anyValue: any = 'Chaine de caractères'; 
let stringValue = <string>anyValue; 

Ou

let stringValue = anyValue as string; 

Il faut avoir en tête que le cast n’est pris en compte que par le compilateur. Le code Javascript généré n’effectue pas de cast à proprement parlé.

Par exemple, pour le code précédent, le code Javascript généré est:

var anyValue = 'Chaine de caractères'; 
var stringValue = anyValue;

Différences entre “let” et “var”

On peut penser que les mot-clés let et var ont la même signification pourtant il existe une différence importante:

  • Les variables définies avec var ont une portée liée à la fonction entière
  • Les variables définies avec let ont une portée liée au bloc de code.

Par exemple, si on écrit:

var numberValue = 45; 
if (true) { 
    var numberValue = 54; 
} 

console.log(numberValue);   // La valeur est 54 car la portée est liée à la fonction. 

Si on remplace var par let:

let numberValue = 45; 
if (true) { 
    let numberValue = 54; 
} 

console.log(numberValue);   // La valeur est 45 car la portée est liée au bloc de code. 

Pour comprendre il faut regarder le code Javascript généré. Dans le cas du var, on a:

var numberValue = 45; 
if (true) { 
    var numberValue = 54; 
} 

console.log(numberValue); 

Dans le cas du let, on obtient;

var numberValue = 45; 
if (true) { 
    var numberValue_1 = 54; 
} 

console.log(numberValue); 

La variable dans le bloc et celle à l’extérieur du bloc n’ont pas le même nom d’où la différence dans le résultat obtenu à l’exécution.

C’est la raison pour laquelle, il est conseillé d’utiliser let plutôt que var car le fonctionnement de let se rapproche de la gestion des variables en C#.

Object literal

Les object literals correspondent à un type Javascript complexe permettant de définir une structure composée de plusieurs données membres:

let dimensions = { 
    Height: 10, 
    Width: 54 
}; 

Le nom des propriétés doit être définie à l’instanciation de l’object literal.

On peut affecter des valeurs aux propriétés après la définition:

dimensions.Height = 7; 
dimensions.Width = 2; 

Le type de l’object literal est implicite dans le cas précédent. On peut être plus explicite avec la notation suivante:

let dimensions: { 
      Height: number, 
      Width: number 
  } = { 
    Height: 10, 
    Width: 54 
}; 

Un object literal est de type object toutefois si on le déclare en explicitant le type object, on ne pourra pas accéder aux propriétés:

let dimensions: object = { 
    Height: 10, 
    Width: 54 
}; 

dimensions.Height = 7; // cette affectation ne fonctionne pas à cause du type object 
dimensions.Width = 2;  // cette affectation ne fonctionne pas à cause du type object 

Il faut définir l’object literal sans préciser le type pour pouvoir accéder aux propriétés:

let dimensions = { 
    Height: 10, 
    Width: 54 
}; 

On peut définir des méthodes dans un object literal.

Déstructuration

La déclaration d’un object literal correspond à la structuration d’une structure de données. La structure construite contient donc plusieurs propriétés. Par la suite, il est possible de déstructurer l’object literal en plusieurs propriétés.

Par exemple, si on définit l’object literal suivant:

let size = { 
    Height: 4, 
    Width: 8, 
    Depth: 2 
}; 

On peut déstructurer l’object literal en plusieurs propriétés distinctes en utilisant la syntaxe suivante:

let {height, width, depth} = size; 

Ainsi, les propriétés sont utilisables individuellement:

console.log(height); 
console.log(width); 
console.log(depth);

Dans le cas où on veut effectuer une affectation sur des variables déjà existantes, il faut utiliser une autre syntaxe:

({height, width, depth} = size);

Déstructuration avec reste

On peut déstructurer un object literal en ne considérant qu’une certaine partie de ses propriétés. Par exemple, si on définit l’object literal suivant:

let point = { 
    X: 4, 
    Y: 8, 
    Z: 2, 
    A: 8, 
    B: 9 
};

Il est possible de ne considérer que les premières valeurs en utilisant la syntaxe:

let {x, y, ...rest} = point;

Ainsi si on affiche le contenu des variables, on aura:

console.log(x); // affiche 4 
console.log(y); // affiche 8 
console.log(rest); // affiche un object literal avec {Z: 2, A: 8, B: 9}

Array

Un tableau peut être déclaré de cette façon:

let valueList: number[] = [5, 9, 2]; 

ou

let valueList: Array<number> = [5, 9, 2]; 

Les 2 notations sont équivalentes.

Déstructuration d’un tableau

On peut déstructurer les valeurs d’un tableau en plusieurs variables de cette façon:

let points = [5, 9, 2]; 

On peut déstructurer les valeurs du tableau en variables distinctes:

let [x, y, z] = points; 

Les variables peuvent ainsi être utilisées individuellement:

console.log(x); 
console.log(y); 
console.log(z); 

Déstructuration d’un tableau avec reste

On peut déstructurer un tableau en ne considérant qu’une certaine partie de ses valeurs. Par exemple, si on définit le tableau suivant:

let points = [4, 8, 2, 8, 9]; 

Il est possible de ne considérer que les premières valeurs du tableau en utilisant la syntaxe:

let {x, y, ...rest} = points; 

Ainsi si on affiche le contenu des variables on aura:

console.log(x); // affiche 4 
console.log(y); // affiche 8 
console.log(rest); // affiche un tableau contenant [2, 8, 9] 

Fonctions

On peut déclarer une fonction de cette façon:

function <nom de la fonction>(<arguments>): <type de retour> { 
    <Corps de la fonction> 
} 

Par exemple:

function Multiply(val1: number, val2: number): number { 
    return val1*val2; 
} 

Le type de retour peut être omis. Dans ce cas, le type de retour est déterminé de façon implicite:

function Multiply(val1: number, val2: number) { 
    return val1*val2; 
} 

Pour indiquer qu’il n’y a pas de types de retour, on peut utiliser void comme en C#:

function LogResult(val1: number, val2: number): void { 
    console.log(val1); 
    console.log(val2); 
} 

Arrow functions

Les arrow functions sont les équivalents des lambda expressions en C#. On peut déclarer une arrow function de cette façon:

var multiplyValues = function(val1: number, val2: number): number { 
    return val1 * val2; 
} 

Une autre syntaxe plus courte est aussi possible en utilisant => (d’où le terme arrow function):

var multiplyValues = (val1: number, val2: number) => val1*val2; 

Pour déclarer une arrow function qui ne renvoie rien, il faut utiliser le mot clé void:

var logValue = (val1: number): void => console.log(val1);

Définir une fonction dans un “object literal”

L’object literal présenté plus haut peut contenir des fonctions membres en plus des données membres.

Par exemple, si on reprend l’exemple précédent:

let dimensions = { 
    Height: 10, 
    Width: 54, 
    CalculateArea: function() { 
        return this.Height * this.Width; 
    } 
}; 

Il faut remarquer l’utilisation de this qui est obligatoire pour accéder aux données membres de l’object literal.

On peut exécuter la fonction de cette façon:

dimensions.CalculateArea(); 

Paramètre optionel

On peut déclarer des arguments optionels dans une fonction avec la syntaxe <nom de l'argument>?: <type de l'argument>.

Par exemple:

function CalculateArea(height: number; width?: number): number 
{ 
    if(width === undefined) {  
        return height * height;  
    } 

    return height * width; 
} 

Pour savoir si l’argument est défini ou non, on peut tester en utilisant la syntaxe:

if(width === undefined) 
{   
    // ...
} 

L’appel à cette fonction peut se faire de cette façon:

CalculateArea(5, 7); 

Ou

CalculateArea(5); 

Utiliser un “object literal”

On peut utiliser des object literals comme argument ou en retour d’une fonction.

Par exemple en argument:

function CalculateArea(rectangle: { height: number, width?: number}): number 
{ 
    if(rectangle.width === undefined) {  
        return rectangle.height * rectangle.height;  
    } 

    return rectangle.height * rectangle.width; 
} 

L’appel à cette fonction se fait en définissant un object literal respectant la signature:

let rectangleVar = { height: 5, width: 8}; 
let area = CalculateArea(rectangleVar); 

On peut utiliser un object literal en type de retour de cette façon:

function DefineRectangle(height: number, width: number): { h: number; w: number } { 
    return { h: height, w: width }; 
} 

Il n’y a pas de difficulté pour utiliser une fonction de ce type:

let rectangle = DefineRectangle(4, 9); 
console.log(rectangle.h); 
console.log(rectangle.w); 

const

Le mot clé const permet de rendre immutable une variable. Il ne faut pas confondre const avec le mot clé readonly. readonly peut être utilisé pour les propriétés d’une classe alors que const doit être utilisé pour les variables d’une méthode.

Ainsi si on écrit:

const immutableValue: number = 34; 

On ne peut plus modifier la valeur de immutableValue par la suite.

const permet d’empêcher de nouvelle affectation sur une variable toutefois elle ne rends pas la contenu de la variable immutable.

Par exemple, si on définit l’object literal suivant:

const size = { 
Height: 6, 
Width: 9 
}; 

On ne pourra pas affecter une nouvelle valeur à la variable size:

size = { 
Height: 1, 
Width: 2 
}; // provoque une erreur 

Toutefois, on peut modifier le contenu de l’object literal:

size.Height = 3; // Ne provoque pas d'erreur 

Il faut donc utiliser const de préférence pour les types primitifs ou les structures de données immutables.

Classe

Les classes en Typescript correspondent à la même notion qu’en C#:

  • Une classe permet d’encapsuler des données membres privées (i.e. fields), des propriétés et des fonctions membres.
  • On peut considérer une classe sous sa forme statique ou sous la forme d’une instance.
  • Une classe peut être instanciée avec un constructeur.
  • Une classe peut hériter d’une autre classe (le mutlihéritage n’est pas possible en Typescript).
  • Une classe peut implémenter une ou plusieurs interfaces.
  • De la même façon que dans la plupart des langages objet, il existe une notion de portée pour les données et fonctions membres:
    • private: lorsque les membres ne sont accessibles qu’à l’intérieur de la classe.
    • protected: lorsque les membres sont accessibles dans le classe et les classes qui en héritent (i.e. classe “enfant”).
    • public: les membres sont accessibles à l’extérieur de la classe.

Concernant la portée des membres d’une classe, il existe une différence importante avec le C#: par défaut si une opérateur de portée n’est pas précisé les membres sont publics. Pour limiter la portée d’un membre, il faut obligatoirement ajouter le mot clé protected ou private.

Une classe se déclare de cette façon:

class Car { 
    Engine: string; // par défaut, Engine est public 

    constructor(engine: string) { 
        this.Engine = engine; 
    } 
} 

Quelques remarques:

  • Un constructeur s’appelle toujours constructor().
  • Pour atteindre un membre à l’intérieur d’une classe, il faut préfixer avec le mot-clé this qui est obligatoire.
  • Sans utilisation d’opérateur de portée, un membre est public par défaut.

Instanciation

L’instanciation de la classe définie précédemment peut se faire de cette façon:

let bigCar: Car; 
bigCar = new Car('V8'); 
console.Log(bigCar.Engine); 

Plus directement, en définissant et en initialisant en une seule ligne:

let bigCar: Car = new Car('V8'); 

Plus simplement, on peut omettre le type (i.e. type inference):

let bigCar = new Car('V8'); 

Instanciation d’une classe en utilisant “typeof”

Il est possible d’instancier une classe avec le type d’une classe obtenue avec l’opérateur typeof:

let carMaker: typeof Car = Car; // Définition du type Car 
let carInstance : Car = new carMaker('V8'); // instanciation de la classe avec le type 

Notation abrégée des membres dans le constructeur

Une notation plus abrégée permet de déclarer des membres directement dans les paramètres du constructeur.

Par exemple:

class Car { 
    constructor(engine: string) { 
    } 
} 

La déclaration précédente indique que engine est une donnée membre privée de la classe Car (sans précision d’opérateur de portée, engine est privée).

Si on précise la portée de engine avec public:

class Car { 
    constructor(public engine: string) { 
    } 
} 

Avec cette définition, on peut donc écrire:

let littleCar = new Car('2CV'); 
console.Log(LittleCar.engine); 

Fonctions

Comme pour les données membres, les fonctions membres sont publiques par défaut et l’accès aux membres se fait obligatoirement avec this.

Par exemple:

class Car { 
    constructor(public engine: string) { 
    } 

    GetCarEngine(): string { 
        return 'The engine is: ' + this.engine; 
    } 
} 

L’utilisation de la fonction est classique:

let littleCar = new Car('2CV'); 
let carEngine = littleCar.GetCarEngine(); 

De même, on peut rendre la fonction privée en utilisant le mot clé private:

class Car { 
    constructor(public engine: string) { 
    } 
    
    DisplayCarEngine() { 
        console.log(this.GetCarEngine()); 
    } 
    
    private GetCarEngine(): string { 
        return 'The engine is: ' + this.engine; 
    } 
} 

Accesseurs

On peut utiliser des accesseurs à partir de ES5. Ces accesseurs peuvent être définis avec les mot clé get et set:

class Car { 
    private engine: string; 
    
    constructor(newEngine: string) { 
        this.engine = newEngine; 
    } 
    
    get power(): string { 
        return this.engine; 
    } 
    
    set power(newEngine: string) { 
        this.engine = newEngine; 
    } 
}  

L’utilisation des accesseurs se fait directement sans utiliser de parenthèses:

let bigCar = new Car('V12'); 
bigCar.power = 'V8'; 
console.log(bigCar.power); 

readonly

readonly a la même signification qu’en C#, il s’applique aux propriétés de façon à forcer son instanciation au moment de la déclaration ou au plus tard dans le constructeur. Il y aura une erreur de compilation si on tente d’affecter la propriété dans le corps de la classe en dehors du constructeur.

Par exemple:

Class Car { 
    readonly passengerCount: number; 
    readonly plateNumber = 'XXXXX'; 
    
    constructor(readonly engine: string) {  
        // on peut ajouter readonly directement dans le constructeur 
    
        this.number = 4; 
    } 
} 

Propriété statique

La notion de propriété statique permet d’affecter une valeur à une propriété d’une classe en dehors d’une instance de façon statique. Pour déclarer une propriété statique, il suffit d’utiliser le mot clé static. L’accès à la propriété se fait en précédant le nom de la propriété avec le nom de la classe.

Par exemple:

class Car {
    static BigEngine: string; 

    constructor(engine: string) { 
    } 
} 

On peut affecter une valeur à la propriété statique sans instancier la classe:

Car.BigEngine = 'V12'; 

La valeur de la propriété statique n’a pas de lien avec l’instance d’une classe:


let car1 = new Car(Car.BigEngine); 
let car2 = new Car('V8');

Atteindre une propriété statique avec “typeof”

On peut atteindre un propriété statique différemment en définissant un type puis en utilisant ce type pour atteindre la valeur statique:

let carMaker: typeof Car = Car;  // définition du type 
carMaker.BigEngine = 'V8'; // On peut atteindre la propriété statique 
console.log(Car.BigEngine);  // Les 2 notations sont équivalentes 

Héritage

Pour qu’une classe hérite d’une autre classe, il faut utiliser le mot clé extends. L’appel au constructeur de la classe se fait en utilisant le mot clé super().

Par exemple, si on définit la classe parent:

class Vehicle { 
    constructor(protected engine: string) { 
    } 
} 

On peut définir la classe Car héritant de Vehicle de cette façon:

class Car extends Vehicle { 
    constructor(carEngine: string) { 
        super(carEngine); 
    } 
    
    GetCarEngine(): string { 
        return this.engine; 
    } 
} 

“Overrider” une méthode

On peut overrider la méthode de la classe parente, pour ce faire il n’est pas nécessaire d’utiliser un mot clé particulier.

Par exemple:

class Vehicle { 
    protected passagerCount: number; 
    
    AddPassager(newPassagerCount: number): void { 
        this.passagerCount = this.passagerCount + newPassagerCounter; 
    } 
} 

On peut overrider directement la méthode AddPassager():

class Motobike extends Vehicle { 
    AddPassager(newPassagerCount: number): void { 
        let newCount = this.Passager + newPassagerCounter;  
        
        if (newCount <= 2) { 
            this.passagerCount = this.passagerCount + newPassagerCounter; 
        }
    }
} 

Si on veut appeler la méthode dans la classe parente, il faut utiliser l’instruction super.<nom de la méthode>(), par exemple:

class Motobike extends Vehicle { 
    AddPassager(newPassagerCount: number): void { 
        let newCount = this.Passager + newPassagerCounter;  
        
        if (newCount <= 2) { 
            super.AddPassager(newPassagerCount); 
        } 
    } 
} 

Effectuer un “cast”

La notion de cast existe en Typescript, elle correspond à la même notion qu’en C#.

Par exemple, si on définit les classes suivantes:

class A {} 

class B extends A {} 

Si on instancie la classe B de cette façon:

let genericInstance: A = new B(); // classInstance est de type A 

1ère notation:
On peut “caster” la classe A en B avec 2 notations qui sont équivalentes:

let specificInstance = <B>genericInstance; 

2e notation:
L’autre notation est:

let specificInstance = genericInstance as B; 

Classe abstraite

Les classes abstraites en Typescript sont proches de celles en C#:

  • On peut déclarer des méthodes abstraites dans une classe abstraite.
  • Les méthodes abstraites ne possédent pas d’implémentation.
  • Pour qualifier les méthodes abstraites et les classes abstraites, il faut utiliser le mot clé abstract.
  • On ne peut pas instancier directement une classe abstraite.

Par exemple, si on définit la classe abstraite suivante:

abstract class Vehicle { 
    protected passagerCount: number; 
    
    abstract AddPassager(newPassagerCount: number): void; 
} 

La classe dérivant de Vehicle doit implémenter la méthode abstraite AddPassager():

class Car extends Vehicle { 
    AddPassager(newPassagerCount: number): void { 
        this.passagerCount = this.passagerCount + newPassagerCount; 
    } 
} 

Generic

La notion de generic est la même qu’en C#, elle permet de définir une classe, une méthode ou une fonction en utilisant un type générique pour une propriété ou un paramètre.

Par exemple, dans le cas d’une classe:

class SimpleClass { 
    SimpleProperty: number; 
} 

Si on utilise un generic, on peut affecter un type générique à la propriété de façon à permettre d’utiliser d’autres types:

class SimpleClass<T> { 
    SimpleProperty: T; 
    
    constructor(simpleProperty: T) { 
        this.SimpleProperty = simpleProperty; 
    } 
}

On peut instancier la classe de cette façon, en précisant le type précis:

let instance: SimpleClass<string> = new SimpleClass<string>(); 

Et utiliser la propriété typée:

let simpleValue: string = instance.SimpleProperty;

Un generic peut aussi être utilisé seulement à l’échelle d’une méthode ou d’une fonction.

Par exemple:

function funcWithGeneric<T>(simpleParameter: T): T { 
    return simpleParameter; 
} 

Combinaisons de types

Il est possible en Typescript de définir des types qui sont des combinaisons d’autres types.

Intersection

L’intersection correspond à un type cumulant tous les membres d’autres types. Ainsi, si on considère le type A et le type B. Si on écrit le type A&B correspondant à l’intersection de A et B, on obtient un nouveau type cumulant les membres des types A et B.

Par exemple, si on définit les 2 types suivants:

class A {
    constructor(public memberA: number) {
    }
}

class B {
    constructor(public memberB: number) {
    }
}

On peut instancier un nouveau type A&B tel que:

let intersection: = <A&B> {};
intersection.memberA = 4;
intersection.memberB = 7;

Il est aussi possible d’utiliser la définition du type pour des arguments de fonctions, par exemple:

function UseIntersection(intersection: A&B): A&B {
    // ...
}

Union

L’union correspond à un type comprenant les membres d’un type ou de l’autre (mais pas les 2 à la fois comme pour l’intersection). Ainsi, si on considère le type A et le type B. Si on écrit le type A|B, on obtient un nouveau type comprenant soit les membres du type A, soit les membres du type B.

Par exemple, si on définit les 2 types suivants:

class A {
    constructor(public memberA: number) {
    }
}

class B {
    constructor(public memberB: number) {
    }
}

On peut définir une fonction utilisant le type A|B:

function UseUnion(unionInstance: A|B) {
    if (unionInstance instanceof A) {
        console.log("Type A" + unionInstance.memberA);
    } 
    else if (unionInstance instanceof B) {
        console.log("Type B" + unionInstance.memberB);
    }
}

Utilisation de “type guards”

Les protections de type (i.e. type guards) permettent de protéger le code en effectuant des vérifications sur le type des variables. Il existe plusieurs opérateurs.

is

Cet opérateur s’utilise dans le type de retour d’une fonction pour indiquer quel est le type précis dans le cas où on utilise un type union.

Par exemple, si on définit la fonction suivante:

function IsA(unionInstance: A|B): unionInstance is A {
    return unionInstance instanceof A;
}

Le retour de la fonction sera vrai seulement si unionInstance est de type A:

let aInstance: A = new A(3);
if (IsA(aInstance)) {
    // Toujours vrai
}

typeof

Cet opérateur permet de vérifier le type d’une variable en le comparant avec le nom du type sous forme de chaine de caractères. Cet opérateur n’est utilisable que pour les types string, number, boolean et symbol. La comparaison ne peut donc s’effectuer qu’avec les chaines de caractères "string", "number", "boolean" ou "symbol".

Par exemple, on peut écrire la fonction suivante qui va effectuer un certain nombre de tests:

function TestType(parameter: string|number|boolean) {
    if (typeof parameter === "string") {
        console.log("string");
    }
    else if (typeof parameter === "number") {
        console.log("number");
    }
    else if (typeof parameter === "boolean") {
        console.log("boolean");
    }
}

instanceof

Cet opérateur permet de tester le type réel d’une variable. La comparaison peut être effectuée sur n’importe quel type (il n’est pas limité comme typeof).

Par exemple, on peut écrire la fonction suivante:

function TestType(parameter: A|B) {
    if (parameter instanceof A) {
        console.log("A");
    }
    else if (parameter instanceof B) {
        console.log("B");
    }
}

Interface

Les interfaces en Typescript sont légèrement différentes des interfaces en C# car il est possible de satisfaire une classe sans utiliser de classe. En effet en Typescript, un object literal peut satisfaire une interface.

On peut définir dans une interface des propriétés et des méthodes, par exemple:

interface ICalculator { 
    calcValue: number; 
    
    Add(value: number): void; 
} 

Définir un objet quelconque satisfaisant une interface

Si on considère l’interface suivante:

interface ISize {  
    Height: number; 
    Width: number; 
} 

On peut définir un objet satisfaisant une interface directement de cette façon:

let dimension: ISize = <ISize>{}; 
dimension.Height = 6; 
dimension.Width = 9; 

Utilisation des interfaces avec des “object literals”

Un object literal peut satisfaire une interface, il n’est pas obligatoire d’utiliser des classes.

Par exemple, dans le cadre de l’interface définie plus haut, on peut définir un object literal de cette façon:

let calculatorExample = { 
    calcValue: 0, 
    Add: function(value: number): void { 
        this.calcValue = this.calcValue + value; 
    } 
}; 

Si on définit une fonction de cette façon:

function GetCalculator(): ICalculator { 
    // ...  
} 

On peut implémenter cette fonction en utilisant directement l’object literal:

function GetCalculator(): ICalculator { 
    let calculatorExample = { 
        calcValue: 0, 
        Add: function(value: number): void { 
            this.calcValue = this.calcValue + value; 
        } 
    }; 
    
    return calculatorExample;  
} 

Implicitement l’object literal va satisfaire l’interface ICalculator.

Ainsi pour savoir si l’object literal satisfait l’interface, le compilateur effectue les vérifications suivantes:

  • Il faut que toutes les propriétés et méthodes déclarées dans l’interface soient présentes dans l’object literal. Toutefois l’object literal peut comporter d’autres propriétés ou méthodes qui ne sont pas déclarées dans l’interface.
  • L’ordre de définition des propriétés ou des méthodes n’a pas d’importance. Il peut être différent de l’ordre utilisé dans l’interface.
  • Si le type d’une propriété ne vérifie pas la déclaration de l’interface, une erreur se produira.

Propriété optionelle

Une interface peut être définie avec des propriétés optionnelles. Elles sont indiquées avec la notation ?:.

Par exemple:

interface ISize { 
    Height: number; 
    Width: number; 
    Depth?: number; 
} 

Si une propriété est optionnelle, il n’est pas obligatoire de la définir dans l’object literal:

let dimension: ISize = { 
Height: 5, 
Width: 8 
}; 

Pour tester et vérifier que la propriété est définie, on peut utiliser la condition:

if (<instance de l'object literal>.<nom de la propriété>) { 
    // ... 
} 

Cette instruction permet de tester si la propriété a la valeur undefined.

Par exemple avec l’object literal dimension défini précédemment:

if (!dimension.Depth) { 
    dimension.Depth = 4; 
} 

readonly

L’utilisation de l’opérateur readonly permet de rendre un objet immutable en empêchant de pouvoir modifier la valeur de ses propriétés.

Ainsi si on définit l’interface suivante:

interface ISize { 
    readonly Height: number; 
    readonly Width: number; 
} 

Si on définit l’object literal suivant:

let dimension: ISize = { 
    Height: 5, 
    Width: 8 
}; 

Aucun changement de valeur ne sera possible sur les propriétés de l’object literal:

dimension.Height = 7; // entraîne une erreur 
dimension.Width = 2; // entraîne une erreur 

Utilisation des interfaces avec des classes

De façon plus classique que les object literals, on peut utiliser des interfaces avec des classes. Pour qu’une classe puisse satisfaire une interface, il faut utiliser le mot clé implements, par exemple:

interface ICalculator { 
    CalcValue: number; 
    Add(value: number): void; 
    Subtract(value: number): void; 
} 

class Calculator implements ICalculator { 
    CalcValue: number = 0; 
    
    Add(value: number): void { 
        this.CalcValue = this.CalcValue + value; 
    } 
    
    Subtract(value: number): void { 
        this.CalcValue = this.CalcValue - value; 
    } 
} 

Héritage entre interfaces

Une interface peut hériter d’une ou de plusieurs interfaces en utilisant le mot clé extends, par exemple:

interface A { 
    PropertyA: number; 
} 

interface B { 
    PropertyB: number; 
} 

interface C extends A, B { 
} 

Si une classe implémente C, elle doit satisfaire A et B:

class Example implements C { 
    PropertyA: number; 
    PropertyB: number; 
} 

Déclarer une signature de fonction sous forme d’interface

On peut utiliser une interface pour déclarer la signature d’une fonction. Par exemple, si on définit l’interface suivante:

interface AddFunction { 
    (operand1: number, operand2: number): number; 
}

On peut utiliser l’interface de cette façon:

let add: AddFunction; 
add = function(operand1: number, operand2: number): number { 
    return operand1 + operand2; 
} 

On peut omettre les indications de types car ils sont implicitement pris en compte:

let add: AddFunction; 
add = function(operand1, operand2) { 
    return operand1 + operand2; 
} 

Déclarer une fonction d’indexation sous forme d’interface

L’indexation étant une fonction particulière, on peut la déclarer de cette façon:

interface IndexStructure { 
    [index: number]: string; 
} 

L’interface peut être utilisée de cette façon:

let arrayStructure: IndexStructure; 
arrayStructure = ['Value1', 'Value2', 'Value3']; 
let value = arrayStructure[1]; 

Interface héritant d’une classe

Une interface peut hériter d’une classe, dans ce cas elle hérite de tous les membres de la classe sans l’implémentation correspondante.

Par exemple, si on définit la classe:

class Calculator { 
    CalcValue: number; 
    Operator(value: number): number { 
        return this.CalcValue + value; 
    } 
} 

Une interface peut dériver de la classe en utilisant le mot clé extends:

interface IOperator extends Calculator { 
    SetCalcValue(newValue: number): void; 
} 

Si une classe satisfait l’interface, elle doit respecter les déclarations de l’interface IOperator et de la classe Calculator et elle doit implémenter les déclarations correspondantes:

class Subtractor implements IOperator { 
    CalcValue: number; 
    Operator(value: number): number { 
        return this.CalcValue - value; 
    } 
    
    SetCalcValue(newValue: number): void { 
        this.CalcValue = newValue; 
    } 
} 

Instructions courantes

On explicite, dans cette partie, la syntaxe de quelques instructions couramment utilisées.

if…then…else

La syntaxe d’instructions if...then...else est assez classique et très similaires au C#:

if (<condition>) { 
    // instructions si la condition est vraie 
} 
else { 
    // instructions si la condition est fausse 
} 

On peut ajouter un if après le else si nécessaire:

if (<condition 1>) { 
    // instructions si la condition 1 est vraie 
} 
else if (<condition 2>) { 
    // instructions si la condition 2 est vraie 
} 

Opérateurs de comparaison

Les opérateurs de comparaison sont identiques à ceux utilisés en C#:

Opérateur Description
> Strictement supérieur à
< Strictement inférieur à
>= Supérieur ou égal à
<= Inférieur ou égal à
== Est égal à
!= N’est pas égal à

Opérateurs logiques

La syntaxe des opérateurs logiques est aussi semblable à celle utilisée en C#:

Opérateur Description
expr1 && expr2 (and) Vraie si les 2 expressions sont vraies
expr1 || expr2 (or) Vraie si au moins une expression est vraie
!expr (not) Permet d’indiquer l’inverse d’une expression logique

Comportement avec “null” et “undefined”

Pour tester si une variable est définie (i.e. égale ou non à undefined), il suffit de tester directement:

if (<nom de la variable>) { 
    // instructions si la variable est différente de "undefined" 
} 

Par exemple:

let numValue : number; // variable égale à 'undefined' 
if (numValue) { 
    // instructions si numValue est défini 
} 
else { 
    // instructions si numValue n'est pas défini 
} 

Le comportement est le même avec null:

let numValue : number = null; // variable égale à 'null' 
if (numValue) { 
    // instructions si numValue est nul 
} 
else { 
    // instructions si numValue n'est pas nul 
} 

Pour éviter la confusion est une valeur égale à null ou undefined, il est préférable d’effectuer une comparaison avec null explicitement:

if (numValue == null) { 
    // ... 
} 

Avec le paramétrage --strictNullChecks

Dans le cas où on utilise le paramétrage --strictNullChecks, l’utilisation d’une variable avec une valeur undefined provoque une exception de type ReferenceError. Dans ce cas, pour vérifier si une variable est définie (i.e. égale à undefined), il faut effectuer la comparaison:

if (typeof variableName !== 'undefined') { 
    // ... 
} 

null et undefined sont des types à part entière toutefois du point de vue de la comparaison, ils sont considérés comme égaux:

  • undefined == undefined est vrai
  • null == undefined est vrai
  • null == null est vrai

undefined ne signifie pas valeur nulle, chaine vide ou valeur fausse:

  • undefined == 0 est faux
  • undefined == '' est faux
  • undefined == false est faux

De même pour null:

  • null == 0 est faux
  • null == '' est faux
  • null == false est faux

Différences entre “==” et “===”

En Javascript, l’opérateur de comparaison == n’a pas la même signification que ===:

  • ==: compare 2 opérandes en tentent d’effectuer des conversions de type entre les variables si nécessaire.
    Par exemple en Javascript:

    • 2 == "2" est vrai car la 2e opérande est convertie en type number pour effectuer la comparaison avec la 1ère opérande.
    • "2" == 2 est aussi vrai
    • 0 == "" est vrai car "" correspond à 0 après la conversion
    • "" == 0 est aussi vrai
    • "0" == "" est faux, dans ce cas il n’y pas de conversion de type.
    • "" == "0" est aussi faux.
  • ===: compare 2 opérandes sans effectuer de conversion au préalable.
    Par exemple:

    • 2 === "2" est faux.
    • "2" === 2 est faux.
    • 0 === "" est faux.
    • "" === 0 est faux.
    • "0" === "" est faux.
    • "" === "0" est faux.

Dans le cadre de Typescript, la vérification du type des variables entraîne des erreurs de compilation si on tente de comparer des variables dont le type n’est pas le même. Toutefois, il est préférable d’utiliser l’opérateur === pour explicitement effectuer les comparaisons sans conversion de type en Javascript.

L’opposé de l’opérateur == est != et l’opposé de === est !==.

Boucles

La plupart des instructions utilisées en C# pour définir des boucles, fonctionnent aussi en Typescript avec une syntaxe semblable.

Boucle “for”

Pour la boucle for, la syntaxe est semblable au C#:

for (let i=0; i< 10; i++) { 
    // ... 
} 

Boucle “while”

Par exemple:

let i: number = 0; 
while (i < 10) { 
    // ... 
    i++; 
} 

Boucle “do…while”

De même que la boucle while:

let i: number = 0; 
do 
{ 
    // ... 
    i++; 
} 
while (i < 10) 

break/continue

L’utilisation de l’instruction break permet de stopper l’exécution d’une boucle comme en C#.

Par exemple:

let i: number = 0; 
while (i < 10) { 
    // ... 
    if (i == 5) break; 
    i++; 
} 

On peut utiliser break avec tous les types de boucles.

Contrairement à break, continue permet de passer à l’itération suivante sans exécuter les instructions se trouvant dans la boucle et après l’instruction continue.

Par exemple:

for (let i=0; i< 10; i++) { 
    if (i == 5) continue; // si continue est exécuté, le reste de la boucle 
                                 // n'est pas exécuté
    // ... 
} 

De même, on peut utiliser continue avec tous les types de boucles.

Boucle infinie

On peut définir des boucles infinies avec la syntaxe suivante pour une boucle for:

let i: number = 0; 
for(;;) { 
    if (i > 10) break; 
    i++: 
} 

Dans le cas de boucle while, la syntaxe est plus classique, par exemple:

while (true) { 
    // ... 
} 

for…of

Pour itérer les éléments d’un tableau ou les caractères d’une chaine de caractères, on peut utiliser l’instruction for...of qui a l’avantage de nécessiter moins de code à écrire.

Par exemple, pour itérer les éléments d’un tableau:

let simpleArray = [7, 4, 8]; 
for (let element of simpleArray) { 
    console.log(element); 
} 
Si le code cible correspond à une version antérieure à ES6

Pour les versions antérieures à ES6, le code javascript généré d’une boucle for...of correspond à une boucle for itérant en utilisant la longueur de la structure (i.e. propriété length):

La boucle précédente devient, après génération en javascript:

let simpleArray = [7, 4, 8]; 
for (let i=0; i< element.length;i++) { 
    console.log(element[i]); 
} 

Pour que l’exécution fonctionne, il faut que la structure possède une propriété length. Dans la cas contraire, l’exécution provoquera une erreur.

Ne pas confondre “for…of” et “for…in”

Contrairement à for...of, for...in permet d’itérer sur les index d’une structure et non sur les éléments de la structure. Ainsi, pour un tableau, si on écrit:

let simpleArray = [7, 4, 8]; 
for (let element in simpleArray) { 
    console.log(element); 
} 

Le résultat sera 0, 1, 2 et non 7, 4, 8.

for…in

L’intérêt de for...in est de pouvoir itérer facilement suivant les membres d’une classe.

Par exemple, si on définit la classe suivante:

class SimpleClass {
    constructor(public a: number, public b: number, public c: number) 
}

Si on exécute le code suivant, on peut itérer sur les membres de la classe:

let simpleClass: SimpleClass = new SimpleClass(3, 2, 1);

for (let member in simpleClass) {
    console.log(member + ': ' + (<any>simpleClass)[member]);
}

On obtient le résultat:

a: 3
b: 2
c: 1

try…catch

La gestion d’exception se fait avec les instructions try...catch. La syntaxe est semblable au C#: on entoure le code avec une instruction try...catch et on lance des exceptions en utilisant throw.

Par exemple:

try { 
    // ... 
} 
catch(e) { 
    console.log(e); 
}

Pour lancer une exception, on peut écrire:

throw new Error('Message exception'); 

Il existe des types d’erreurs plus précis que Error. Ces types héritent de la classe Error:

  • RangeError: survient dans le cas où on utilise un index en dehors de l’intervalle des éléments d’une structure.
  • ReferenceError: survient si on utilise une référence invalide.
  • SyntaxError: dans le cas d’une erreur de syntaxe.
  • TypeError: ce type d’erreur se produit si le type utilisé n’est pas valide.

Pour conclure…

Comme indiqué plus haut, le but de cet article était d’expliciter certains éléments de la syntaxe de base de Typescript. Les sujets comme l’export de classes, les modules, les interfaces et l’utilisation de bibliothèque Javascript n’ont pas été traités car ils feront l’objet d’un article futur.

One response... add one

Je reste admiratif face au travail de qualité que tu as produit et je te remercie grandement pour ce partage de connaissance.
Un seul mot ! continue dans cette lancée et beaucoup de courage

Leave a Reply