1257 views
TP à la découverte de 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. [TOC] # TypeScript: JavaScript With Syntax For Types Typescript a explosé en popularité en 2019 et continue sa folle course. C'est le premier langage de programmation a être présent dans le top 10 des langages les plus utilisés en moins de 5 ans d'existence. ## La génè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. <!-- > > TG: OK les vidéos c'est cool... mais pour moi c'est un peu hors sujet. J'ai peur qu'ils passent tout leur temps dedans au lieu de commencer le TP. En fait, je trouve que **ton texte explicatif est très bien et se suffit à lui même**! Je garderai juste la photo qui résume très bien l'état de fait en 2010.--> <!-- <iframe width="761" height="428" src="https://www.youtube.com/embed/Uo3cL4nrGOk" title="Interview with Senior JS Developer" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>--> 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. ![](https://i.imgur.com/YJYNV2b.jpg) 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 ([extrait de ce moment d'histoire pour notre petite communauté IT (pour regarder un soir tard, pas pendant le TP)](https://www.youtube.com/embed/QHulaj5ZxbI?start=1077)). <!--<iframe width="761" height="428" src="https://www.youtube.com/embed/QHulaj5ZxbI?start=1077" title="ng-conf 2015 Day 1 Keynote - Brad Green and Igor Minar" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>--> ## Rappel: c’est quoi Typescript ? Typescript est un langage de programmation open source fait par Microsoft. Pour être plus précis, c’est un surensemble 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 vrai orienté objet, pas d’orienté objet via prototype comme en Javascript. Le fait que Typescript soit un vrai 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. # Configuration de votre environment pour les TPs ## Step 1: Installation de nodejs à l'aide de nvm ### NodeJs c'est quoi ? **NodeJs** est un environnement d’exécution single-thread, open-source et multi-plateforme permettant de créer des applications rapides et évolutives côté serveur. Il fonctionne avec le moteur d’exécution JavaScript V8 et utilise une architecture d’entrées/sorties non bloquante et pilotée par les événements, ce qui le rend efficace et adaptée aux applications en temps réel. ### NodeJs: pourquoi j'en ai besoin dans ce cours ? Même si l'essentiel du code que nous allons utiliser dans ce cours s'exécutera au final dans la navigateur, l'ensemble des outils de la chaîne de compilation (build, dépendance, test) sont eux mêmes écrits en JavaScript ou TypeScript. Ces outils requièrent donc un interpréteur JavaScript installé sur le poste de développeur, dans notre cas nous utiliserons NodeJs. ### NodeJs: pourquoi je ne l'installe pas NodeJs avec le package manager de ma distribution linux ou avec le magasin d'application de Microsoft ? En tant que développeur, on est souvent amené à travailler sur différents projets. JavaScript continue à évoluer et de nombreuses versions de NodeJs existent pour suivre l'évolution du langage, de sa standardisation mais aussi suivre l'évolution des OSs et de leurs fonctionnalités. De ce fait, il est courant que l'on ait besoin de passer d'une version de NodeJs à une autre en conservant un environement cohérent. C'est le rôle de NVM. ### Installation de NVM sous linux, mac ou windows WSL ```bash # exécution du scipt pour installer nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash # lancement d'un nouveau shell bash # Déplacer nvm sur /tmp mv ~/.nvm /tmp export NVM_DIR=/tmp/.nvm # installation de la version 20 de nodejs nvm install 20 # Utilisation de la version 20 par défaut pour cet utilisateur nvm use 20 # Test pour vérifier la version de nodejs utilisée node --version ``` [1] [Documentation du projet NVM (linux, mac)](https://github.com/nvm-sh/nvm) ### Installation de NVM sous windows à l'ISTIC Pour les utilisateurs windows, les commandes sont presque les mêmes. Sur les postes de l'istic, il faudra utiliser la version *nvm-noinstall.zip*, la décompresser dans un répertoire et ajouter le chemin vers **ce répertoire** dans la variable d'environnement $PATH de windows. 1. Dans Rechercher, lancez une recherche et sélectionnez : Système (Panneau de configuration) 2. Cliquez sur le lien Paramètres système avancés. 3. Cliquez sur Variables d'environnement. Dans la section Variables système recherchez la variable d'environnement PATH et sélectionnez-la. Cliquez sur Modifier. Si la variable d'environnement PATH n'existe pas, cliquez sur Nouvelle. 4. Dans la fenêtre Modifier la variable système (ou Nouvelle variable système), indiquez la valeur de la variable d'environnement PATH (ici le répertoire où vous avez placez nvm). Cliquez sur OK. Fermez toutes les fenêtres restantes en cliquant sur OK. 5. Ouvrez à nouveau la fenêtre d'invite de commande et exécutez nvm. Dans l'invite de commande, les commandes restent alors ```bash # installation de la version 20 de nodejs nvm install 20 # Utilisation de la version 20 par défaut pour cet utilisateur nvm use 20 # Test pour vérifier la version de nodejs utilisée node --version ``` [2] [Documentation du projet NVM (windows)](https://github.com/coreybutler/nvm-windows) ## Step 2: Initialisation de votre premier projet TypeScript Dans l'interface de commande, tapez les commandes suivantes: ```bash # génération d'un nouveau projet mkdir tp1secuwebl3 cd tp1secuwebl3 npm init ``` Cela crée le fichier *package.json* qui en résumé conserve une description du projet, ses dépendances, ses scripts de build, ... > 👨🏽‍🏫: La commande "npm init" est utilisée pour générer un fichier *package.json* dans votre répertoire de projet, qui contient un ensemble d'information importantes pour votre projet, son nom, les dépendances utilisées, les scripts utilisés pour construire le projet, .... Une fois répondu aux différentes questions (vous pouvez laisser les valeurs par défaut sauf pour les auteurs et le nom du projet), installons typescript. ```bash # Installons typescript npm install -g typescript ``` Initialisons l'usage de typescript dans notre projet. ```bash # Initialisons typescript pour ce projet tsc --init ``` cela crée le fichier tsconfig.json qui en résumé conserve les options du compilateur typescript. > 👨🏽‍🏫: La commande "tsc --init" est utilisée pour générer un fichier tsconfig.json dans votre répertoire de projet, qui contient des options de compilation et des paramètres pour votre projet TypeScript. En personnalisant les options du fichier tsconfig.json, vous pouvez adapter le processus de compilation TypeScript à votre projet. ### Création d'un répertoire de source à part. La création d'un répertoire *src* distinct pour vos fichiers TypeScript est une pratique courante dans le développement web moderne. Voici quelques raisons pour lesquelles c'est important : - **Organisation** : En créant un répertoire "src" distinct, 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 "src" indique 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 "src" dans votre projet TypeScript, vous pouvez suivre les étapes suivantes : 1. Ouvrez votre terminal ou votre invite de commande. 2. Naviguez jusqu'au répertoire racine de votre projet TypeScript. 3. Tapez la commande suivante pour créer un nouveau répertoire "src" dans votre projet : ```bash mkdir src ``` 4. Cela créera un nouveau répertoire "src" dans votre répertoire de projet. 5. Vous pouvez maintenant déplacer vos fichiers TypeScript dans le répertoire "src". Il est recommandé de créer des sous-répertoires dans le répertoire "src" pour mieux organiser vos fichiers si votre projet est important. > En résumé, la création d'un répertoire "src" distinct pour vos fichiers TypeScript peut contribuer à l'organisation et à la clarté de votre projet, ainsi qu'au respect des meilleures pratiques en matière de séparation et de structure du code. Pour créer un répertoire "src", il suffit d'utiliser la commande "mkdir" dans votre terminal ou à l'invite de commande. ### 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 surensemble de JavaScript. Cependant, les fichiers TypeScript sont différents des fichiers JavaScript classiques car ils sont conçus pour être compilés en code JavaScript qui peut être exécuté dans un navigateur web ou sur un serveur. Pour ajouter des fichiers TypeScript au répertoire "src" de votre projet, procédez comme suit : 1. Ouvrez votre terminal ou votre invite de commande. 2. Naviguez jusqu'au répertoire "src" de votre projet TypeScript. 3. Créez un nouveau fichier TypeScript en tapant la commande suivante : ```bash touch app.ts ``` 4. Cela créera un nouveau fichier TypeScript nommé "app.ts" dans le répertoire "src". 5. Vous pouvez maintenant ajouter du code TypeScript au fichier "app.ts". Le code TypeScript peut inclure des fonctionnalités qui ne sont pas encore prises en charge par JavaScript, telles que les interfaces, les classes et les types. Nous allons aussi demander au compilateur typescript 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é fichier js. Pour ce faire ajouter la ligne suivante dans le fichier *tsconfig.json* (dans la sous-partie "compilerOptions") ```js "outDir": "./dist/", /* Specify an output folder for all emitted files. */ ``` On en profitera pour mettre l'option *inlineSourceMap* à *true*. C'est très pratique en mode debug car cela permet de ne pas debuguer le code js resulat de la compilation du typescript mais de debuguer le ts d'origine. 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 *tsconfig.json*, toujours dans la sous-partie "compilerOptions", on ajoutera la ligne suivante. ```js "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ ``` Pour compiler des fichiers TypeScript en JavaScript, vous pouvez utiliser la commande "tsc", qui est l'interface de ligne de commande du compilateur TypeScript. Voici les étapes à suivre : 1. Ouvrez votre terminal ou votre invite de commande. 2. Naviguez jusqu'au répertoire racine de votre projet TypeScript. 3. Exécutez la commande suivante pour compiler vos fichiers TypeScript : ```bash tsc ``` 4. Cette commande compilera 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 tsconfig.json. La compilation des fichiers TypeScript en JavaScript est nécessaire car les navigateurs web et les interpréteurs côté serveur comme nodejs 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 compilation des fichiers TypeScript en JavaScript est nécessaire pour rendre votre code compatible avec les navigateurs et les interpréteurs Js s'exécutant coté serveur comme NodeJs, et peut être réalisée à l'aide de la commande "tsc". ### Ajout d'un comportement simple dans le fichier typescript. Nous allons maintenant ajouter un simple comportement dans le fichier typescript. Ce comportement va afficher du log (une trace) dans la console. Dans votre fichier ts (app.ts), ajouter la ligne et sauver les modifications ```ts console.error("Un jour j'écrirai un code sans bug"); ``` 1. Recompiler votre fichier ts à l'aide de la commande tsc. 2. executer votre application ```bash # Recompiler votre fichier ts tsc # executer votre application node dist/app.js ``` :warning: Cet é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 re-compilation. > 👨🏽‍🏫: Ok l'initialisation du projet est terminé. 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 initialiser un projet typescript et utiliser NodeJs pour exécuter ce code sur notre machine. # En avant pour un premier travail ## Step 3: Vos premières lignes de code en TypeScript Nous allons dans ce TP 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 appelerons permissions étranges par exemple le fait qu'un sous répertoire est d'avantage de droit qu'un de ses parents ou un fichier avec une extensions connues 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 de notre projet. 👨🏽‍🏫: Ces classes peuvent être définies dans le fichier app.ts préalablement créé. ### Q1: Créer - une classe **Fichier** en TypeScript qui possède un nom, une extension, une taille, des permissions - une classe **Répertoire** qui contient une liste de **FsElement**, un nom et une taille - Une interface **FsElement** qui contient un nom et une taille. Les classes **Fichier** et **Repertoire** implanteront l'interface **FsElement**. Créer un ou deux objets Fichiers et répertoire et afficher les caractéristiques de ces objets dans la console en utilisant *console.log(obj);* Compiler et éxécuter votre code pour vérifier que tout fonctionne correctement. Vous pourrez utiliser l'enum suivante pour modéliser la notion de permission. ```ts enum Permission { Read = 'r', Write = 'w', Exec = 'x', Sticky = 's', } ``` [3] https://www.typescriptlang.org/docs/handbook/2/classes.html ### Q2: Pour la classe **Répertoire**, nous souhaitons que l'attribut taille soit juste un *getter*. Modifiez la définition de l'interface **FsElement** pour définir l'attribut taille comme *readonly* et la classe **Repertoire** pour que cette dernière utilise la notion de getter pour l'attribut taille. Le code du *getter* ne fera que faire la somme de toute la taille de ces fils. Créer un ou deux objets **Fichier** et **Répertoire** et afficher les caractéristiques de ces objets dans la console en utilisant *console.log(obj);* Compiler et éxécuter votre code pour vérifier que tout fonctionne correctement. Entre autre vérifier la mise en oeuvre de votre getter surla taille d'un répertoire. [4] https://www.typescriptlang.org/docs/handbook/2/classes.html#getters--setters [5] https://www.typescriptlang.org/docs/handbook/2/classes.html#readonly ## Step 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, nodejs, ...) sont soit potentiellement lentes (téléchargement de ressources, ..) soit dépendent de l'interaction de l'utilisateur (attente d'un clic de souris, ...). 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, 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 callbacks, - les promesses, - l'utilisation des mots-clefs async/await. ### 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ée son exécution, une des fonctions en attente est retirée de la pile et est exécutée à son tour, c'est l'*event loop*. De base en Javascript, les fonctions sont synchrones et les accès I/O sont asynchrones (accès fichiers, requêtes HTTP, ...). La communauté a essayé de faire converger le format des méthodes dites de 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 mozilla: [7] https://developer.mozilla.org/fr/docs/Learn/JavaScript/Asynchronous/Introducing ### Les appels asynchrones: quelles conséquences ? :warning: 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. **Une première erreur classique quand on débute en asynchrone** Nous allons dans la suite de ce TP utiliser l'API filesystem de nodejs qui permet de faire des actions sur votre système de fichier. Nous allons donc tout d'abord indiquer à typescript que nous allons utiliser la librairie standard de NodeJs. Dans votre terminal, exécuter la commande suivante: ```bash Indique à typescript que nous allons utiliser la librairie standard de nodeJs. npm install -D @types/node ``` Modifiez le fichier tsconfig.json pour ajouter la partie ```json "types": [], ``` doit devenir ```json "types": ["node"], ``` et commentez l'entrée ```json // "verbatimModuleSyntax": true, ``` Puis dans notre fichier *app.ts*, ajoutons le code suivant. ```ts import * as fs from "fs" let content = undefined; fs.readFile('./dist/app.js', (err,data) => { content = data; }); console.log(content); ``` Ici la portion de code `(err,data) => { content = data; });` est la fonction de callback. C'est une fonction *anonyme* dont les deux paramètre sont `err` et `data`. Cette fonction de callback 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 ce cette fonction, Javascript donnera au paramètre `err` la valeur de l'erreur et au paramètre `data` la valeur du résultat. Compiler et exécutez le programme précédent. Vous verrez que la variable *content* reste nulle. En effet, la fonction de *callback*, ici la portion de code `(err,data) => {...}`, dont l'effet est d'affecter *data* à *content* est executé **après** `console.log(content)`. Effectivement, ce style de programmation ne nous est pas naturel. Pire deux fonctions de callback l'une en dessous de l'autre ne s'exécuteront pas forcément de haut en bas. Prenons l'exemple suivant: ```ts 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') }); ``` Compiler et regarder l'ordre d'exécution des callback. Vous verrez qu'il n'est pas celui que l'on attendait en lisant le code de manière linéaire (Il 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. **Gestion des erreurs** <!-- Il y a différentes manières de gérer les erreurs qui vont se produire à l'appel d'une fonction asynchrone. 1. Certaines fonctions asynchrones peuvent recevoir plusieurs fonctions de callback en arguments: - la première est généralement celle qui est appelée lors de la fin d'exécution de la fonction avec un résultat, - la seconde est généralement celle qui est appelée lors d'une exécution interrompue par une exception. Cela donne un code qui ressemble à cela. ```ts monappelAsync(arg, (data)=> { /* résultat si tout se passe bien */}, (err)=> { /* call back en cas de problème */ } ); ``` En NodeJs, ils n'utilisent pas plusieurs fonction de callback mais une seule avec une convention sur cette fonction de callback que l'on appelle *error-first callback*. --> En NodeJs, tous les appels asynchrones prennent une seule méthode de callback. Cette méthode de callback 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 callback dans ce style de programmation. ```ts // The following file does not exists const file = "file.txt" // This should throw an error // Executing the function fs.readFile(file, // ErrorFirstCallback (err, data) => { if (err) { return console.log("Error: " + err); } console.log("Function successfully returned: ", data); } ) ``` ### L'enfer des callbacks Les callbacks peuvent devenir infernaux à gérer lorsque l'appel à une fonction asynchrone va dépendre 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 (*readir*, *stat* et *chmod*). ```ts 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 callbacks 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èles soient terminés, devient vite "infernal" => on parle de "**callbacks hell**". Dans ces situations, il vaut mieux utiliser un autre mécanisme: les **promesses**. ### 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 callback à 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: ```ts const maPromesse = new Promise((resolve, reject) => { ... traitement lent ... construisant une valeur résultat x // déclenche la résolution de la promesse avec comme paramètre: x resolve(x); ... ... // traitement de l'échec de la promesse reject(...); }); ``` 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. ```ts const maPromesse = new Promise((resolve, reject) => { // traitement lent // Ici, on calcule la factorielle 100 et on place le résultat dans x. let x = 1 for (let i = 2; i < 1000; i++) { x = x * i } if (x === Infinity) { // ici on applique la fonction de callback // gérant les erreurs en lui passant la raison de l'erreur reject(new Error("Overflow")) } else { // ici, on déclenche la résolution de la promesse avec comme paramètre: x resolve(x) } }) maPromesse .then(result => console.log("Succes: ", result)) .catch(err => console.log("Error: ", err)) ``` Si l'on revisite le code précédent (vérifiant la présence d'un fichier `file.txt` avec des promesses, nous obtiendrons ceci. Remarque: Il faut changer l'importation pour utiliser l'API NodeJs qui utilise les promesses pour *fs*. ```ts import * as fs from "fs/promises"; /* au lieu de import * as fs from "fs";*/ fs.readFile("./file.txt") .then(data => console.log("Function successfully returned: ", data)) .catch(err => console.log("Error: ",err)) ``` ### 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. ```ts 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 comportement de plusieurs promesses (*Promise.any()*, *Promise.race()*, *Promise.allSettled()*). N'hésitez pas à aller voir [la documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) pour comprendre leurs usages. ### É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. ```ts 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 2. 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: ```ts const v= await f(1) console.log(v) ``` Mais, à la place, on doit empacter notre appel à `f` dans une fonction asynchrone: ```ts 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. A noter que le code équivalent écrit avec des promesses et des `.then` serait: ```ts 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`. ```ts async function mamethodeDeLectureDeFichier(){ const data = await fs.readFile("./dist/app.js"); } ``` Le code équivalent à l'exemple sur les callback hell est donné ci-dessous. J'espère vous convaincre qu'il est plus facile à relire :smiley_cat: . <!-- > TG ben bof :smirk: comme je ne sais pas ce que fait fs.stat ... et puis c'est aussi fait exprès pour que ça soit imbouffable non? Genre `4 & parseInt((stats.mode & parseInt("777", 8)).toString(8)[0])` WTF? > --> ```ts 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 fichier 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"); ``` **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. :heart: ```ts // Using the fs module through import import * as fs from "fs/promises"; // The following file does not exists const file = "file.txt"; async function mamethodeDeLectureDeFichier(){ 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. ## Step 5: Petit projet ### Q3: A partir de la [documentation de l'API du FS](https://nodejs.org/api/fs.html), fabriquer une méthode *printFileWithSizeEqualNul* attaché à la classe *Repertoire* qui, à partir de la structure de données préalablement créée (**Fichier**, **Répertoire**, **FsElement**), explore ce graphe pour détecter les fichiers donc la taille est zero. On rappelle que la taille d'un répertoire est la somme des tailles de ces éléments. Il faudra préalablement créer la fonction qui crée le graphe d'objet instance de Repertoire et Fichier à partir du chemin d'un répertoire donné en paramètre. :ticket: Pour lister les fichier d'un répertoire, vous pouvez utiliser la fonction ```ts fs.readdir(path) ``` :ticket: Pour savoir si un élement contenu dans un répertoire est un répertoire ou un fichier, la fonction *fs.stat()* vue précédemment retourne un objet sur lequel on a accès à l'API suivante. ```ts stats.isDirectory() ``` :ticket: De même, pour connaître la taille d'un fichier à l'aide de l'api *fs*; on utilisera l'attribut *size* de l'objet retourné par *fs.stat()*. ```ts stats.size ``` :ticket: Enfin, n'oubliez pas vos cours sur la récursivité afin de créer un graphe complet. :ticket: Automatisation de la phase de compilation. Recompiler à chaque fois que l'on souhaite tester l'exécution, c'est fastidieux. Il est classique d'utiliser des mécanismes qui vont surveiller l'état du système de fichier et déclencher la compilation pour vous. Pour ce faire, à la racine du projet, lancer la commande: ```bash tsc --watch ``` Cela lance un processus qui surveille tous vos fichiers typescript et relance la compilation quand un fichier *ts* est modifié. ### Q4: Etendez votre structure de données pour ajouter la notion de permission et vérifier de manière récursive qu'aucun enfant n'a des permissions moins restrictives qu'un de ses parents. De nouveau, on modifiera la fonction qui permet de créer le graphe d'objets Repertoire et Fichier à partir d'un chemin donné sous forme de chaine de caractères et on ajoutera une méthode à la classe répertoire pour la partie vérification de droits. ## Step 6: 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é 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'entres elles. **noImplicitAny** 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 desactive 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 .... 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*. **Contrôles stricts de nullité** Par défaut, les valeurs telles que null et undefined sont assignables à 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 les union de type. ```ts 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: ```ts name?: string ``` ### Q5: Dans notre classe *Fichier*, ajoutez un attribut *est_lie_a* qui en tant que référence optionnel vers un autre objet. Je vous donne la syntaxe dans l'extrait de code ci-dessous ```ts est_lie_a?: FsElement // FsElement est le nom de votre interface créée à la question 1 ``` Créer ensuite une méthode *affiche_est_lie_a* qui affiche dans la console le nom de l'élement auquel est lié un fichier. Pour cela nous allons tester plusieurs syntaxe ```ts affiche_est_lie_a():void{ console.log('lié à :', this.est_lie_a.nom) } ``` ```ts affiche_est_lie_a():void{ if (this.est_lie_a !== undefined){ console.log('lié à :', this.est_lie_a.nom) } } ``` ```ts affiche_est_lie_a():void{ console.log('lié à :', this.est_lie_a?.nom) } ``` ```ts C ``` Après un échange avec votre intervant de TP, la lecture des références suivantes, présenter 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. [6] https://askjavascript.com/exclamation-mark-or-bang-operator-in-typescript-when-dereferencing/ [7] https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#optional-properties [8] https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties ## Step 7: Placement de ce projet sur git Maintenant que le squelette est en place, nous allons placer ce code sur git. Créer un *blank project* sur le gitlab de l'istic. https://gitlab2.istic.univ-rennes1.fr/ :warning: Désélectionnez *Initialize repository with a README* afin de garder un repo complètement vide. ![](https://i.imgur.com/rQY9d7D.png) Insérer votre clé publique dans gitlab. Vous pouvez suivre le tutoriel suivant. https://docs.gitlab.com/user/ssh/ Puis quand votre clé a été insérée. ```bash # Configurer git sur votre machine, # ⚠️ à remplacer avec votre nom et votre mail git config --global user.name "Barais Olivier" git config --global user.email "olivier.barais@univ-rennes1.fr" # Initialiser le repo local git init --initial-branch=main # Initialiser le repo distant sur lequel vous enverrez votre code # ⚠️ mettre l'url en fonction de votre repo créé sur gitlab git remote add origin git@gitlab2.istic.univ-rennes1.fr:obarais/tp1secuwebl3.git # Création d'un fichier Readme.md pour placer votre rapport touch Readme.md #Pour votre future rapport # Ajout des fichiers à suivre au sein de l'historique git add Readme.md src/** package.json tsconfig.json # Commit des modifications git commit -m 'Initial commit' # Premier push vers le repo distant en fixant que la branche local courante est à placé vers la branch main à distance git push --set-upstream origin main # à partir de là, à la fin de chaque question, vous pouvez faire git commit . # afin de créer un nouveau snapshot de l'historique de vos fichiers # et git push # pour envoyer l'historique vers gitlab # ⚠️ si vous créez de nouveaux fichiers, il faudra les ajouter à l'index en utilisant la commande git add. # N'oubliez pas de regarder par moment l'état de votre repo en faisant un git status. # 👨🏽‍🏫:La qualité de l'utilisation de git sera évalué dans la note globale des TPs ``` ## Step 8: Et les tests unitaires dans tout cela? ### Q6 : On va créer un test qui vérifie le comportement du getter de taille pour la classe Réperoire. ``` 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 à : ```jsx= /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { preset: 'ts-jest', testEnvironment: 'node', }; ``` dans le fichier package.json, ajoutez les lignes suivantes ```js "scripts": { "test": "jest src/ --coverage" }, ``` Cela permet de lancer le runner de test quand on lance la commande: ```bash npm run test ``` Enfin créer votre premier test qui test la classe Repertoire ```bash touch src/app.test.ts ``` avec le contenu suivant ```ts import {describe, expect, test} from '@jest/globals'; describe('sum module', () => { test('adds 1 + 2 to equal 3', () => { expect(1+ 2).toBe(3); }); }); ``` :warning: pour importer la classe **Repertoire**, cette dernière doit être exportée. Dans le fichier *app.ts*, ajoutez le mot clé **export** devant les définitions (fonction, classe, ...) que vous souhaitez exporter. Par exemple pour exporter la classe Fichier modifiez l'entête `class Fichier{...}` en `export class Fichier{...}`. Dans votre fichier de test `src/app.test.ts`, vous allez devoir importer les fonctions, classes, etc. exportées par `src/app.ts`. Pour cela ajoutez une directive `import` au début de `src/app.test.ts`. Par exemple, `import {Fichier, Repertoire} from './app.ts';` Compléter le fichier de test avec des tests vérifiant que la valeur retournée par le champ `taille` de la classe répertoire est correcte. :warning: N'oubliez pas d'ajouter le *jest.config.js* et vos fichiers de tests à votre repo git ```bash git add jest.config.js src/index.test.ts git commit . -m 'add test' git push ``` ## Step 9: 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](https://typescript-eslint.io/). Vous devez installer ESLint. ESLint ne supporte pas nativement TypeScript, vous devez donc également installer eslint-typescript-support :+1: ```bash npm install --save-dev eslint @eslint/js typescript typescript-eslint jiti ``` La commande ci-dessus ajoute ESLint, ajoute un analyseur qui permet à ESLint de comprendre TypeScript, et ajoute quelques règles spécifiques à TypeScript. Ensuite, créez un fichier de configuration nommé *eslint.config.mjs* à la racine de votre projet, et remplissez-le avec ce qui suit : ```jsx= import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; export default tseslint.config( eslint.configs.recommended, tseslint.configs.recommended, ); ``` **Exécuter ESLint**: Ouvrez un terminal à la racine de votre projet et exécutez la commande suivante : ```bash npx eslint src/ ``` N'hésitez pas à aller voir la documentation spécifique. https://typescript-eslint.io/getting-started/ ## Consignes de rendu Le code de ce TP sera à rendre sur gitlab. Vous nous transmettrez l'URL du git et le noms,prenoms du binome dans un formulaire qui vous sera transmis plus tard. Votre repository doit un contenir un fichier Readme.md contenant un compte rendu associé à ce TP.