Introduction à TypeScript
À la découverte de TypeScript, des appels asynchrones, des callbacks, des promesses, des mots clés await, async et du test à l’aide de Jest.
1. Crédits
La version initiale de cet exercice a été réalisée par Olivier Barais. La version originelle est disponible ici.
2. Étape 1 : TypeScript: JavaScript avec des types
TypeScript a explosé en popularité en 2019 et continue sa folle course. C’est le premier langage de programmation à être présent dans le top 10 des langages les plus utilisés en moins de 5 ans d’existence.
2.1. La genèse
Nous sommes en 2010. Tout le monde se plaint de JavaScript, mais JavaScript est partout. Pour votre culture sociologique du monde des développeurs, nous vous joignons la vidéo ci-dessous.
En 2010, chez les clients externes de Microsoft, tout le monde est d’accord sur un point. Le Javascript n’est pas fait pour les projets à grandes échelles. Mais Javascript est quand même utilisé dans ces projets de grande envergure. Ces projets sont en Javascript pour une raison simple : les navigateurs n’acceptent que le Javascript ! Tout le monde est bloqué avec.
C’est avec ce problème en tête que Microsoft va commencer à travailler sur TypeScript. Le projet va être développé pendant deux ans en interne. En octobre 2012, la version 0.8 de TypeScript passe publique pour la première fois. Explosion en 2015 aussi quand Google invite le responsable du projet TypeScript de chez Microsoft à sa conférence (ng-conf), conférence organisée par Google à destination des développeurs Web. Quand deux grands acteurs en forte concurrence converge sur un choix technique, l’impact sur le domaine IT est forcément important.
2.2. Rappel: c’est quoi TypeScript ?
TypeScript est un langage de programmation open source fait par Microsoft. Pour être plus précis, c’est un sur-ensemble de Javascript. C’est-à-dire que tout programme Javascript existant est déjà un programme TypeScript valide.
TypeScript est un langage multi paradigmes. Un développeur peut faire du fonctionnel, comme de l’orienté objet sans problème. Et nous parlons de vrais objets, pas d’orienté objet via prototype comme en Javascript. Le fait que TypeScript soit un langage à objet et fortement typé a tout changé. Le système de type particulièrement avancé et flexible a renforcé l’attrait pour ce langage.
TypeScript ne remplace pas JavaScript.
Le modèle de développement est simple, le développeur développe en TypeScript dans des fichiers .ts.
Le compilateur TypeScript (tsc) traduira une application écrite en TypeScript en une application Javascript.
Développer en TypeScript permet de sécuriser une partie du développement.
En particulier, l’utilisation des types permet d’éviter quantité de bugs lors de la phase de compilation. Après compilation, on est en Javascript.
Ce code Javascript est interprété par le navigateur et est exposé au même risques de sécurité que du code directement écrit en Javascript.
Mais comme le développeur est passé par TypeScript au préalable, la sûreté de l’application est « renforcée ».
Et c’est ça qui fait le succès de TypeScript.
Passons à la pratique.
3. Étape 2 : Initialisation de votre premier projet TypeScript
La commande posera différentes questions. Vous pouvez laisser les valeurs par défaut, sauf pour les auteurs et le nom du projet. |
Cela crée le fichier package.json qui en résumé conserve une description du projet,
ses dépendances, ses scripts de build, etc.
|
La commande Ce fichier contient un ensemble d’information concernant le projet, comme son nom, les dépendances utilisées, les scripts utilisés pour le construire/produire, etc. |
|
Cette commande crée le fichier tsconfig.json qui en résumé conserve les options du compilateur TypeScript.
|
La commande En personnalisant les options du fichier |
3.1. Création d’un répertoire source
La création d’un répertoire src distinct pour vos fichiers TypeScript est une pratique courante dans le développement web moderne.
En Voici quelques raisons `:
-
Organisation : En créant un répertoire
srcdistinct, vous pouvez séparer vos fichiers TypeScript des autres fichiers du projet, ce qui facilite la navigation et l’organisation de votre projet. -
Clarté : Le répertoire
srcindique clairement où se trouvent les fichiers source de votre projet TypeScript, ce qui facilite la compréhension et la contribution des autres développeurs à votre projet. -
Séparation des préoccupations : Séparer vos fichiers sources des autres fichiers du projet permet de s’assurer que votre projet est bien organisé et qu’il respecte les meilleures pratiques en matière de structure de code et de séparation des préoccupations.
|
Pour créer un répertoire
|
-
Cela créera un nouveau répertoire
srcdans votre répertoire de projet. -
Vous pouvez désormais placer vos fichiers TypeScript dans le répertoire
src. Il est recommandé de créer des sous-répertoires dans le répertoiresrcpour mieux organiser vos fichiers, si votre projet est important.
|
En résumé, la création d’un répertoire Pour créer un répertoire |
3.2. Ajouter et compiler des fichiers TypeScript dans le répertoire src
Les fichiers TypeScript sont similaires aux fichiers JavaScript ordinaires en ce sens qu’ils contiennent du code écrit dans le langage TypeScript, qui est un sur-ensemble de JavaScript.
Cependant, les fichiers TypeScript sont différents des fichiers JavaScript classiques, car ils sont conçus pour être d’abord traduits en code JavaScript qui peut être exécuté dans un navigateur web ou sur un serveur.
|
Pour ajouter des fichiers TypeScript au répertoire
Cela créera un nouveau fichier TypeScript nommé |
Vous pouvez maintenant ajouter du code TypeScript au fichier app.ts.
Le code TypeScript peut inclure des fonctionnalités qui n’étaient pas
prises en charge par JavaScript avant la norme ES6 (ES2015+),
telles que les interfaces et les classes,
ainsi que des fonctionnalités qui ne sont pas présentes dans la norme actuelle de JavaScript,
ES2023, comme les types natifs, les types génériques, les annotations et les énumérations.
Maintenant, nous allons préciser au compilateur TypeScript que les fichiers sources se trouvent dans le répertoire ./src/.
Nous allons aussi lui demander de placer les résultats de compilation dans le répertoire ./dist/ pour ne pas mélanger le code source (fichiers .ts) et le code compilé (fichiers .js).
|
Pour ce faire, ajouter les ligne suivante dans le fichier
|
On en profitera pour mettre l’option inlineSourceMap à true.
C’est très pratique en mode debug car cela permet de déboguer directement le code TypeScript,
au lieu de déboguer le code JavaScript résultant de la traduction.
Cette option indique au compilateur TypeScript d’inclure les éléments de traçabilité entre le code js généré par le compilateur et le fichier .ts d’origine (c’est ce que l’on appelle les sourcemap`).
|
Dans le fichier
|
Pour compiler des fichiers TypeScript en JavaScript, vous pouvez utiliser la commande tsc, qui est l’interface de ligne de commande du compilateur TypeScript.
|
Si vous installé TypeScript localement, grâce à nom, avec |
|
Voici les étapes à suivre :
Cette commande traduira tous les fichiers TypeScript de votre projet et générera les fichiers JavaScript correspondants dans le répertoire de sortie spécifié dans le fichier |
La traduction des fichiers TypeScript en JavaScript est nécessaire, car les navigateurs web et les interpréteurs côté serveur comme Node.js ne peuvent exécuter que du code JavaScript.
En compilant les fichiers TypeScript en JavaScript, vous pouvez rendre votre code TypeScript compatible avec les navigateurs web et les serveurs et lui permettre d’être exécuté sur ces plateformes.
|
En résumé, la traduction des fichiers TypeScript en JavaScript est nécessaire pour rendre votre code compatible avec les navigateurs et les interpréteurs JavaScript s’exécutant coté serveur, comme Node.js, et peut être réalisée à l’aide de la commande |
3.3. Ajout d’un comportement simple dans le fichier TypeScript.
Nous allons maintenant ajouter un comportement simple dans le fichier TypeScript. Ce comportement va afficher du log (une trace) dans la console.
|
Dans votre fichier
|
|
à la racine de votre projet, compilez et exécutez votre code grâce aux commandes suivantes :
|
|
Cette étape de compilation est à effectuer à chaque fois que vous souhaitez exécuter une nouvelle version de votre programme. Par la suite, nous verrons comment automatiser la compilation. |
|
L’initialisation du projet est terminée. Cela vous permet de travailler proprement à partir de là, nous rentrons dans le vif du sujet. Ces étapes d’initialisation peuvent être reprises pour d’autres projets personnels utilisant du TypeScript. Nous avons initialisé un projet TypeScript et utilisé Node.js pour exécuter ce code sur notre machine. |
4. Étape 3 : Vos premières lignes de code en TypeScript
Nous allons maintenant créer un programme écrit en TypeScript pour scanner notre système de fichiers afin de détecter des fichiers et des répertoires avec des permissions étranges.
Nous appellerons permissions étranges par exemple le fait qu’un sous-répertoire ait davantage de droits qu’un de ses parents ou un fichier avec une extension connue comme un document (.txt, .csv, .docx) avec des permissions d’exécutions par exemple.
Pour cela commençons par créer les structures de classes[1] de notre projet.
Ces classes peuvent être définies dans le fichier app.ts préalablement créé.
|
Les classes
|
|
|
5. Étape 4 : Comprendre la programmation asynchrone
|
L’exécution de fonctions en JavaScript est, en général, synchrone. Cependant, certaines fonctions proposées par les environnements hôtes (le navigateur, Node.js, etc.) sont soit potentiellement lentes (par ex. le téléchargement de ressources), soit dépendent de l’interaction avec l’utilisateur (attente d’un clic de souris, etc.). Afin de ne pas bloquer l’exécution du programme (ou le chargement du reste de la page web), celles-ci ont un fonctionnement asynchrone, c’est à dire, non bloquant. |
Dans cette étape, nous allons présenter les différents modèles de programmation proposés pour la gestion des appels à des fonctions asynchrones, tels que:
-
Les fonctions de rappel (callback),
-
Les promesses,
-
L’utilisation des mots-clefs
asyncetawait.
5.1. Pourquoi des appels asynchrones ?
Le moteur d’exécution de JavaScript est mono-thread; une seule instruction peut être exécutée à la fois. Lorsqu’une fonction, considérée comme asynchrone, est appelée, celle-ci est placée sur une pile de fonctions en attente d’exécution, pour ne pas bloquer l’exécution de la fonction courante.
Lorsque la fonction courante a terminé son exécution, une des fonctions en attente est retirée de la pile et est exécutée à son tour, c’est la boucle événementielle, ou Event Loop.
De base en JavaScript, les fonctions sont synchrones et les accès I/O sont asynchrones (accès fichiers, requêtes HTTP, etc.). La communauté a essayé de faire converger le format des méthodes dites de rappel, ou callback. Les méthodes qui sont appelées quand l’exécution de la fonction JavaScript a eu lieu.
Pour bien comprendre la différence entre fonction synchrone et asynchrone, nous vous conseillons la lecture de l’introduction faite par la Fondation Mozilla.
5.2. Les appels asynchrones : quelles conséquences ?
|
La programmation de fonctions utilisant des fonctions asynchrones doit tenir compte du fait que les résultats des appels à ces dernières peuvent être obtenus dans un ordre et après un délai aléatoires. C’est souvent très perturbant, car le cerveau humain a tendance a naturellement penser synchrone et execution ligne à ligne. En particulier, si une ligne de code est juste en dessous d’une autre alors on s’attend à ce qu’elle soit exécutée après. Ce n’est pas forcément vrai en programmation asynchrone. |
5.3. Une première erreur classique quand on débute en asynchrone
Nous allons dans la suite de ce TP utiliser le module filesystem de Node.js,
qui permet de faire des actions sur votre système de fichiers.
Nous allons donc tout d’abord indiquer à TypeScript que nous allons utiliser la librairie standard de Node.js.
|
Ici, la
fonction fléchée
(err,data) ⇒ {content = data;});
est la fonction de rappel.
C’est une fonction anonyme, dont les deux paramètre sont err et data.
Cette fonction de rappel est passée en paramètre à la fonction readFile().
Que la fonction readFile() s’arrête sur une erreur ou sur un résultat, Javascript exécutera la fonction de callback.
Lors de l’appel de cette fonction, Javascript donnera au paramètre err la valeur de l’erreur et au paramètre data la valeur du résultat.
|
En effet, la fonction de rappel, ici la portion de code (err,data) ⇒ {…},
dont l’effet est d’affecter data à content est exécutée après l’instruction console.log(content).
Effectivement, ce style de programmation ne nous est pas naturel. Pire encore, deux fonctions de rappel, l’une en dessous de l’autre, ne s’exécuteront pas forcément de haut en bas.
Prenons l’exemple suivant :
fs.readFile('./dist/app.js', (err,data) => {
console.log('readFile');
});
fs.readdir('/tmp', (err,data) => {
console.error('/tmp');
});
fs.readdir('/usr/lib', (err,data) => {
console.error('/usr/lib');
});
Vous verrez qu’il n’est pas celui que l’on attendait en lisant le code de manière linéaire. Le code n’est même pas déterministe, si l’on regarde la spécification du runtime JavaScript. |
Donc même si vous exécutez ce code en ayant l’impression que l’exécution est déterministe, elle pourrait être différente sur un autre interprète Javascript ou une autre machine.
5.4. Gestion des erreurs
En Node.js, tous les appels asynchrones prennent une seule méthode de rappel. Cette méthode prend deux paramètres d’entrée. Le premier paramètre est un objet représentant l’exception. Le second est un objet représentant le résultat. Si l’objet exception est vide alors l’exécution de l’appel asynchrone s’est passé sans difficulté.
Voici un exemple de fonction de rappel dans ce style de programmation :
const file = "file.txt"; (1)
fs.readFile(file, (2)
(err, data) => {
if (err) {
return console.log("Error: " + err);
}
console.log("Function successfully returned: ", data);
}
)
| 1 | Fichier inexistant |
| 2 | L’exécution doit lever une erreur |
5.5. L’enfer des rappels
Les fonctions de rappel peuvent devenir infernales à gérer lorsque l’appel à une fonction asynchrone dépend du résultat d’une autre fonction asynchrone.
Supposons que nous devons vérifier les permissions des fichiers d’un répertoire et ajouter systématiquement le droit en lecture à l’utilisateur courant si ce dernier ne l’a pas.
Cela donnerait un code comme celui-là,
car on combine trois appels de fonctions asynchrones suivantes : readdir(), stat() et chmod().
fs.readdir(folder, (err, data) => {
if (err) {
console.error("cannot read this folder");
return;
}
data.forEach((file) => {
fs.stat(folder + "/" + file, (error, stats) => {
if (error) {
console.error("cannot get permission for this file", file, error);
return;
} else {
if (!(4 & parseInt((stats.mode & parseInt("777", 8)).toString(8)[0]))) {
console.error("change permission for ", file);
fs.chmod(
folder + "/" + file,
"" + ((stats.mode & parseInt("777", 8)) + 400),
(error1) => {
if (error1) {
console.error("cannot change permission for this file", file);
return;
}
}
);
}
}
});
});
});
L’imbrication des appels asynchrones en passant les fonctions de rappel en cas de réussite et en cas d’erreur, sans parler des cas où il faut s’assurer que tous les appels à des fonctions asynchrones exécutées en parallèle soient terminés, devient vite infernal ! On parle de "Callback Hell".
Dans ces situations, il vaut mieux utiliser un autre mécanisme : les promesses.
6. Étape 5 : Les promesses
Une promesse est un objet Promise, standardisé depuis ES6 et qui permet une simplification de l’écriture de fonctions asynchrones.
Un objet Promise est instancié en lui fournissant,
en argument la fonction asynchrone à exécuter qui prend elle-même en arguments,
la fonction de rappel à exécuter en cas de succès (resolve) et celle à exécuter en cas d’erreur (reject).
Ainsi, on peut créer une promesse avec un code de la forme :
const myPromise = new Promise((resolve, reject) => {
... traitement lent
... construisant une valeur résultat x
resolve(x); (1)
...
...
reject(...); (2)
});
| 1 | Déclenche la résolution de la promesse avec comme paramètre: x |
| 2 | Traitement de l’échec de la promesse |
Voici un exemple simple de promesse dont le rôle est de calculer la factorielle de 1000 (c’est notre traitement lent). Ce calcul peut donner un résultat ou échouer dans le cas d’un débordement.
const myPromise = new Promise((resolve, reject) => {
//
let x = 1
for (let i = 2; i < 1000; i++) { (1)
x = x * i
}
if (x === Infinity) {
reject(new Error("Overflow")); (2)
} else {
resolve(x); (3)
}
})
myPromise
.then(result => console.log("Succes: ", result))
.catch(err => console.log("Error: ", err))
| 1 | Traitement lent. On calcule la factorielle 100 et on place le résultat dans x. |
| 2 | On applique la fonction de rappel gérant les erreurs en lui passant la raison de l’erreur |
| 3 | On déclenche la résolution de la promesse avec comme paramètre: x |
|
6.1. Combiner plusieurs promesses
L’avantage est alors de pouvoir faire aussi simplement des points de synchronisation. Imaginons que je lance trois appels asynchrones et que je souhaite continuer quand ces trois appels sont terminés.
const promises = [];
promises.push(fs.readFile("./dist/app.js"));
promises.push(fs.readdir("/usr/lib"));
promises.push(fs.readdir("/tmp"));
Promise.all(promises).then( ... )
La méthode Promise.all() renvoie une promesse qui est résolue lorsque l’ensemble des promesses contenues dans l’itérable passé en argument ont été résolues ou qui échoue avec la raison de la première promesse qui échoue au sein de l’itérable. (voir Promise.all())
Il existe d’autres méthodes pour obtenir un pattern de coordination quand on doit combiner les comportements de
plusieurs promesses (Promise.any(), Promise.race(), Promise.allSettled()).
N’hésitez pas à aller voir
la documentation
pour comprendre leurs usages.
6.2. Écrire des fonctions asynchrones comme des fonctions synchrones: async/await
ES7 a introduit un support au niveau langage de la notion de promesse afin d’éviter l’utilisation de la fonction then().
Il a introduit deux nouveaux mots-clefs permettant de simplifier davantage l’écriture de fonctions asynchrones pour qu’elles ressemblent un peu aux fonctions synchrones.
Dans ce modèle, la déclaration d’une fonction asynchrone doit être précédée du mot clé async et
son résultat sera nécessairement une promesse.
Prenons, par exemple, la déclaration de fonction suivante :
async function f(x: number): Promise<number> {
for (let i = 0; i < 1000000000; i++) { }
return x + 1
}
Avec cette fonction, si on réalise un appel de la forme const v= f(1) on obtiendra (immédiatement)
un objet de type Promise<number>, c’est-à-dire, la promesse d’un résultat de type number à venir.
Cette promesse sera associée à v.
Cependant, il est possible d’obtenir le résultat (et non juste une promesse) en ajoutant devant l’appel de fonction le mot clé await.
En clair, la signification de await ici est d'attendre la résolution de la promesse.
Ainsi, si on écrit const v = await f(1), l’exécution sera bloquée jusqu’à la résolution de la promesse et la production du résultat.
On a retrouvé la synchronisation.
Petite restriction, await ne peut être utilisé que dans des fonctions, elles-mêmes asynchrones.
Ainsi, pour tester notre exemple, on ne peut pas écrire directement :
const v = await f(1);
console.log(v);
Mais, à la place, on doit empaqueter notre appel à f() dans une fonction asynchrone :
async function test() {
const v = await f(1);
console.log(v);
}
test();
Les deux instructions de la fonction test seront bien exécutées dans l’ordre.
À noter que le code équivalent écrit avec des promesses et des then() serait :
function test() {
f(1).then(data => console.log(data));
}
test();
L’utilisation des await/async simplifie grandement l’écriture des fonctions utilisant des fonctions asynchrones.
Voici le code qui permet de récupérer l’objet représentant le descripteur du fichier ./dist/app.js.
async function myFileReaderFunction(){
const data = await fs.readFile("./dist/app.js");
}
Le code équivalent à l’exemple sur l’enfer des rappels est donné ci-dessous. Nous espérons vous convaincre qu’il est plus facile à relire :
async function checkFolder(folder:string){
const data = await fs.readdir(folder);
for (const file of data){
const stats = await fs.stat(folder + "/" + file)
// (stats.mode & parseInt("777", 8) est un moyen de passer les droits sur le système de fichiers en base 8.
// On récupère ensuite la valeur entre 0 et 7 du premier chiffre. C'est les droits pour l'utilisateurs courant. Petite explication ici
// https://astuces-informatique.com/que-signifie-chmod-777/
if (!(4 & parseInt((stats.mode & parseInt("777", 8)).toString(8)[0]))) {
console.error("change permission for ", file);
await fs.chmod( folder + "/" + file,
"" + ((stats.mode & parseInt("777", 8)) + 400));
}
}
}
checkFolder("/tmp");
6.3. Gestion d’erreur
La gestion d’erreur doit être mise en place en tenant compte qu’une fonction déclarée asynchrone par async retourne une promesse.
Nous utiliserons alors un try catch classique :
import * as fs from "fs/promises";
const file = "file.txt";
async function readMyFile(){
try{
const data = await fs.readFile(file);
} catch (e){
console.error(e);
}
}
Dans la suite du TP, on vous incite fortement à utiliser la notation à l’aide des await et async.
Cette notation permet également d’utiliser les promesses avec .then() si besoin.
7. Étape 6 : Petit projet
À partir de la documentation de l’API du FS,
fabriquez une méthode appelée printFileWithNullSize()),
attachée à la classe Folder qui, à partir de la structure de données préalablement créée (MyFile, Folder, FSElement),
explore ce graphe pour détecter les fichiers donc la taille est zéro.
On rappelle que la taille d’un répertoire est la somme des tailles de ses éléments.
Il faudra préalablement créer la fonction qui crée le graphe d’objet instance de Folder et MyFile,
à partir du chemin d’un répertoire donné en paramètre.
|
Pour lister les fichiers d’un répertoire, vous pouvez utiliser la fonction suivante :
|
|
Pour savoir si un élément contenu dans un répertoire est un répertoire ou un fichier,
la fonction
|
|
De même, pour connaître la taille d’un fichier à l’aide de la bibliothèque
|
| Enfin, n’oubliez pas vos cours sur la récursivité afin de créer un graphe complet. |
|
Automatisation de la phase de compilation.
Cela lancera un processus qui surveille tous vos fichiers TypeScript et relance la compilation quand un fichier |
|
De nouveau, on modifiera la fonction qui permet de créer le graphe d’objets Folder et MyFile,
à partir d’un chemin donné sous forme de chaine de caractères et on ajoutera une méthode à la classe Folder pour la partie vérification de droits.
8. Étape 7 : Comprendre certains aspects du système de type
Par défaut, en TypeScript nous avons pas mal de flexibilité sur la configuration du typechecker (configuration au niveau du fichier tsconfig.json).
En effet, différents utilisateurs viennent à TypeScript en recherchant différentes choses dans un vérificateur de type. Certains recherchent une expérience plus souple qui peut aider à valider seulement certaines parties de leur programme, tout en disposant d’un outil décent. C’est l’expérience par défaut avec TypeScript (pas celle configurée dans notre projet), où les types sont optionnels, l’inférence prend les types les plus indulgents et il n’y a pas de vérification pour les valeurs potentiellement nulles ou indéfinies. Ces valeurs par défaut sont mises en place pour ne pas vous gêner. Si vous migrez du JavaScript existant, cela peut être une première étape souhaitable.
En revanche, beaucoup d’utilisateurs préfèrent que TypeScript valide le plus possible dès le départ et c’est pourquoi le langage fournit également des paramètres de demande de rigueur forte, c’est notre cas dans ce projet. Utiliser une demande de rigueur plus forte peut nécessiter un peu de travail supplémentaire, mais en général cela se paie sur le long terme et permet des vérifications plus approfondies et des outils plus précis. Lorsque cela est possible, une nouvelle base de code devrait toujours activer ces vérifications.
Prenons l’exemple de deux d’entre elles : (i) l’interdiction de l’inférence à Any et
(ii) le contrôle strict de la nullité.
8.1. Interdiction de la référence à Any
Rappelons qu’à certains endroits, TypeScript n’essaie pas de déduire les types pour nous et
se rabat sur le type le plus indulgent : Any.
Ce n’est pas la pire chose qui puisse arriver — après tout, se rabattre sur any est juste l’expérience JavaScript ordinaire de toute façon.
Any est le void de C++, cela désactive toute vérification de type.
Un objet de type Any peut être affecté à n’importe quelle variable quelque soit son type,
on peut lui appeler n’importe quelle méthode, etc.
Cependant, l’utilisation de Any va souvent à l’encontre de la raison d’être de TypeScript.
Plus votre programme est typé, plus vous obtiendrez de validation et d’outils,
ce qui signifie que vous rencontrerez moins de bugs lorsque vous coderez.
Activer le drapeau noImplicitAny dans le fichier tsconfig.json provoquera une erreur
sur toutes les variables dont le type est implicitement déduit comme étant Any.
8.2. Contrôles stricts de nullité
Par défaut, les valeurs telles que null et undefined peuvent être affectées à n’importe quel autre type.
Cela peut faciliter l’écriture de certains codes,
mais oublier de gérer les valeurs null et undefined est la cause d’innombrables bogues dans le monde — certains considèrent que c’est l’erreur à un milliard de dollars !
L’option strictNullChecks rend la gestion de null et undefined plus explicite et
nous évite de nous soucier de savoir si nous avons oublié de gérer null et undefined.
Mettre cette valeur à true empêchera par exemple aussi de laisser une variable déclarée comme une chaîne de caractère au type non défini.
D’un point de vue typage, null et undefined ne deviennent plus des valeurs acceptables pour les types existants.
Pour autoriser une valeur undefined,
il faudra alors dire explicitement qu’une variable peut être une chaîne de caractère, par exemple, ou undefined en utilisant l’union de types :
name: string | undefined
Nous pourrons aussi utiliser la notion de propriété optionnelle si c’est un attribut d’une classe, d’une interface, un paramètre d’une fonction à l’aide de la notation suivante :
name?: string
|
Dans la classe |
On vous donne la syntaxe dans l’extrait de code ci-dessous :
isLinkedTo?: FSElement; (1)
| 1 | FSElement est le nom de l’interface créée précédemment. |
|
Créez ensuite une méthode
|
Après un échange avec votre encadrant de TP, la lecture des références suivantes, présentez dans le compte rendu de TP avec vos mots les avantages et/ou les inconvénients de chacun des codes ci-dessus. Certains codes peuvent provoquer des erreurs à la compilation ou à l’exécution. Vous discuterez le pourquoi de ces erreurs.
9. Étape 8 : Placement de ce projet sur GitLab
Maintenant que le squelette est en place, nous allons placer ce code sur git.
Créez un blank project sur le GitLab de l’Université.
|
Désélectionnez Initialize repository with a README afin de garder un repo complètement vide.
|
|
Configurez git sur votre machine :
|
10. Étape 9 : Et les tests unitaires dans tout cela ?
|
On va créer un test qui vérifie le comportement du getter de taille pour la classe npm install --save-dev jest @types/jest @jest/globals ts-jest npx ts-jest config:init |
Cela crée le fichier de configuration pour le driver de tests (ici on utilise jest). Le fichier généré au départ ressemble à :
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
|
Dans le fichier
|
Cela permet de lancer le runner de test quand on lance la commande :
npm run test
|
Enfin, créez votre premier test qui test la classe
Avec le contenu suivant :
|
|
Pour importer la classe Par exemple pour exporter la classe
|
|
|
11. Étape 10 : Mise en place d’un linter
Un Linter est un outil d’analyse de code qui permet de détecter les erreurs et les problèmes de syntaxe.
Linter son code permet de le rendre :
-
Plus facile à relire et corriger en cas de besoin
-
Plus fiable
Les linters sont configurables : il suffit de décrire les modifications dans un fichier et de le partager aux membres de son équipe. C’est en partageant les configurations que tout le monde pourra travailler selon les mêmes règles. Si vous utilisez certaines technologies, il faut généralement les indiquer dans votre configuration, ou télécharger et inclure des règles de configuration, comme ça le linter pourra s’adapter à vos technologies.
Prenons un exemple : Si votre code est dédié à être exécuté sur un serveur,
il ne doit pas utiliser des fonctionnalités du navigateur comme l’objet window par exemple.
Dans votre configuration du Linter, vous pouvez indiquer que le code est dédié au serveur et lorsque vous écrirez window,
le Linter va se manifester et vous lever une erreur.
|
Il est primordial de maintenir un code uniforme et homogène, pour cela l’utilisation d’un outil de formatting est essentiel ! Ainsi, il sera possible de détecter des bugs potentiels rapidement. À noter que certains outils peuvent envoyer des alertes (plus ou moins gentilles) pour vous avertir de potentiels problèmes. |
Dans la communauté JavaScript, il existe de très nombreux linters. Pour ce cours, nous utiliserons ESLint.
Vous devez installer ESLint.
Comme ESLint ne supporte pas nativement TypeScript,
vous devez donc également installer le module eslint-TypeScript-support:
La commande ci-dessus ajoute ESLint et un analyseur qui permet à ESLint de comprendre TypeScript, ainsi que quelques règles spécifiques à TypeScript. |
|
Ensuite, créez un fichier de configuration .eslintrc.js à la racine de votre projet et remplissez-le avec ce qui suit :
|
|
Pour exécuter ESLint, ouvrez un terminal à la racine de votre projet et exécutez la commande suivante :
|
N’hésitez pas à aller voir la documentation spécifique.
