c) Introduction au TS
Le TS, c'est quoi ?
Le TypeScript est du JavaScript avec des types. Ainsi, toute la syntaxe décrite dans l'introduction du JS est valable pour le TS.
Le TS s'écrit tant dans un browser que dans un environnement serveur.
On écrira du TS conforme au standard que l'on souhaite voir associé à JS : ECMAScript
, CommonJS
...
Comme le monde de l'entreprise va de plus en plus vers ECMAScript 6
(ou ES6
), nous avons choisi ECMAScript
comme standard pour ce cours.
Nous allons voir dans cette partie les spécificités utiles du TS.
Notons que le TS permet d'améliorer l'orienté objet en JS, mais nous ne verrons pas ces aspects dans ce cours sauf pour quelques exceptions. Nous estimons que le développement web moderne ne nécessite pas de maîtriser l'orienté objet en JS ; il nous semble plus intéressant de faire de la programmation fonctionnelle, tout en manipulant des objets et des types.
A quel moment le TS intervient ?
En TypeScript, l'intervention se fait principalement au moment de la transpilation (on parle aussi de compilation par abus de langage) :
- Type Checking: TypeScript intervient lors de la transpilation, vérifiant les types et s'assurant que le code respecte les définitions de types fournies. Cela permet de détecter les erreurs de type avant l'exécution.
- Type Safety : TypeScript aide à maintenir la sécurité des types en obligeant à définir des types précis pour les variables, les fonctions, etc. Cela évite l'utilisation du type
any
, qui désactive les vérifications de type, et réduit les risques d'erreurs liées aux pointeurs nuls (null pointers). - Le code TypeScript est transpilé en JavaScript. Les annotations de type sont supprimées et le code résultant est du pur JavaScript.
Il y a d'autres aspects importants où TypeScript peut intervenir, principalement :
- Intellisense : Les éditeurs de code et IDE qui supportent TypeScript (comme Visual Studio Code) utilisent les informations de type pour fournir des suggestions de code, auto-compléter les noms de variables et de fonctions, et offrir des informations sur les signatures de fonctions.
- Erreurs en temps réel : Pendant l'écriture du code, les éditeurs peuvent afficher des erreurs de type en temps réel, avant même que le code ne soit compilé.
- Documentation : Les annotations de type servent de documentation vivante, aidant les développeurs à comprendre les interfaces et les attentes des fonctions.
- Refactoring : Les outils de refactoring utilisent les informations de type pour effectuer des transformations de code de manière sécurisée (comme par exemple le
Quick Fix...
de VS Code). - Linting : Les outils comme ESLint utilisent les types pour imposer des règles de style et de bonnes pratiques de code.
- Tests : Les frameworks de tests peuvent utiliser les types pour générer des cas de test ou vérifier les types des données manipulées.
- Runtime (indirectement) : Même si TypeScript ne vérifie pas les types à l'exécution, les développeurs peuvent utiliser des gardes de type (type guards) et des assertions pour vérifier les types à l'exécution, ce qui ajoute une couche supplémentaire de sécurité.
Quand définir les types ?
Introduction
Pour maintenir la sécurité des types, il est important d'éviter que TypeScript infère le type any
pour les variables, les fonctions, les paramètres, etc.
Il y a beaucoup de cas où TS est capable d'inférer le type d'une variable, d'une fonction, etc. sans que l'on ait besoin de le spécifier explicitement.
Cas où il est inutile de spécifier le type
👍 Lorsque l'inférence de type est claire et évidente, ou lorsque l'annotation (de type) n'apporte pas de valeur ajoutée significative en termes de lisibilité ou de documentation, il n'est pas recommandé de spécifier le type explicitement. Cela peut rendre le code plus verbeux et moins lisible.
Voici quelques exemples où il est inutile de spécifier le type :
tsconst x = 10; // TypeScript infère que x est de type numberconst y = 'hello'; // TypeScript infère que y est de type stringconst z = [1, 2, 3]; // TypeScript infère que z est de type number[]function greet(name: string) {return `Hello, ${name}!`; // Le type string est évident ici}
Cas où il est important de spécifier le type
Type Checking
👍 Il est recommandé de spécifier le type lorsque TypeScript ne peut pas inférer le type correctement, ou lorsque l'inférence de type peut entraîner des erreurs potentielles difficiles à détecter.
Voici un exemple concret où il est recommandé de spécifier le type :
tslet value; // TypeScript infère que value est de type anyfunction setValue(newValue) {value = newValue;}setValue(42);// Plus tard dans le codeconsole.log(value.toFixed(2)); // Erreur à la compilation/transpilation : toFixed n'est pas une fonction sur type 'any'
TypeScript détectera l'erreur lors de la transpilation/compilation, car value est de type any
et n'a pas de méthode toFixed
.
C'est l'éditeur de code qui indiquera cette erreur avant que le code ne soit transpilé en JavaScript.
Pour éviter ce problème, voici comment on peut spécifier le type de value
:
tslet value: number;function setValue(newValue: number) {value = newValue;}setValue(42);// Maintenant, TypeScript sait que value est de type numberconsole.log(value.toFixed(2)); // Correct : affiche '42.00'
En spécifiant value
comme étant de type number
, TypeScript peut vérifier statiquement que les opérations ultérieures sur value
(comme toFixed(2)
) sont appropriées et éviter les erreurs potentielles qui seraient révélées à l'exécution.
Documentation & lisibilité
👍 Pour l'aspect documentation et lisibilité, il est recommandé de spécifier le type des paramètres de fonction. Concernant les valeurs de retour, même si TypeScript peut les inférer correctement, il est conseillé de spécifier le type de retour lorsque le corps de la fonction est volumineux. Cela rend le code plus explicite et aide les autres développeurs à comprendre comment utiliser la fonction sans avoir à lire son implémentation.
Voici un exemple d'une fonction assez volumineuse où il est recommandé de spécifier le type de retour :
tsfunction processData(data: string[]): { averageLength: number, maxLength: number } {let totalLength = 0;let maxLength = 0;for (let item of data) {totalLength += item.length;if (item.length > maxLength) {maxLength = item.length;}}const averageLength = data.length > 0 ? totalLength / data.length : 0;return { averageLength, maxLength };}
En spécifiant le type de retour { averageLength: number, maxLength: number }
, on documente clairement que la fonction processData produit un objet avec ces deux propriét és. Cela rend le code plus explicite et facilite la compréhension pour les autres développeurs qui utilisent ou maintiennent cette fonction.
Maintenabilité
👍 Dans le cas où le type de retour est complexe ou utilisé à plusieurs endroits dans le code, il est intéressant de définir une interface ou un type pour ce type de retour. Cela permet de réutiliser le type de retour dans d'autres parties du code et de garantir la cohérence des types.
Voici ce que ça donnerait pour notre exemple :
tsinterface DataProcessingResult {averageLength: number;maxLength: number;}function processData(data: string[]): DataProcessingResult {let totalLength = 0;let maxLength = 0;for (let item of data) {totalLength += item.length;if (item.length > maxLength) {maxLength = item.length;}}const averageLength = data.length > 0 ? totalLength / data.length : 0;return { averageLength, maxLength };}
En définissant l'interface DataProcessingResult
, on peut réutiliser ce type de retour dans d'autres parties du code, ce qui rend le code plus maintenable et évite les erreurs de type.
Comment définir les types ?
Il existe plusieurs façons de définir des types en TypeScript. Voici les principales méthodes :
Les annotations de type
Les annotations de type sont des instructions qui indiquent au compilateur TypeScript le type d'une variable, d'un paramètre de fonction, d'une valeur de retour, etc. Les annotations de type sont placées après le nom de la variable, du paramètre ou de la fonction, suivies de deux points :
et du type souhaité.
Voici quelques exemples d'annotations de type :
tslet x: number; // x est de type numberlet y: string; // y est de type stringlet z: number[]; // z est de type number[]const numbers: number[] = [1, 2, 3]; // numbers est de type number[]let isActive: boolean; // isActive est de type booleanlet greet: (name: string) => string;// greet est une fonction qui prend un paramètre de type string et retourne une valeur de type stringconst person: { name: string, age: number } = { name: "Alice", age: 30 };
Les interfaces
Les interfaces sont des contrats qui définissent la structure des objets en TypeScript. Elles permettent de définir des types personnalisés pour les objets, les fonctions, les classes, etc. Les interfaces sont largement utilisées pour définir des types complexes et réutilisables.
Voici un exemple d'interface pour définir un type de données :
tsinterface Person {readonly id: number; // Propriété en lecture seulename: string;age: number;email?: string; // Propriété optionnelle}const alice: Person = { id:1, name: "Alice", age: 30 };const bob: Person = { id:2, name: "Bob", age: 25, email: "bob@vinci.be" };// Tentative de modification d'une propriété en lecture seule (erreur)// alice.id = 3; // Erreur: Cannot assign to 'id' because it is a read-only property.
Notons qu'une interface peut étendre un type défini ou une autre interface. Voici un exemple d'interface qui étend un type défini:
tstype Employee = { // fonctionne aussi avec une interface (interface Employee { ... })name: string;age: number;};interface Manager extends Employee {department: string;manageTeam(): void;}const manager: Manager = {name: "Bob",age: 35,department: "HR",manageTeam() {console.log("Managing team...");}};
Les types
Les types sont similaires aux interfaces, mais ils peuvent également être utilisés pour définir des types primitifs, des unions, des intersections, des tuples, etc.
Voici un exemple de type pour définir un type de données :
tstype Person = {name: string;age: number;email?: string; // Propriété optionnellereadonly id: number; // Propriété en lecture seule}const person: Person = {name: "Alice",age: 30,id: 1// email est optionnel et peut être omis si nécessaire};
Notons que les types peuvent être utilisés pour définir des types primitifs, des unions, des intersections, des tuples, etc. :
tstype ID = string | number; // Unions typetype Printable = {print(): void;};type Loggable = {log(): void;};type LoggableAndPrintable = Printable & Loggable; // Intersections de type// Utilisation du type intersectionlet obj: LoggableAndPrintable = {print() {console.log("Printing...");},log() {console.log("Logging...");}};function readPizzaById(id: number): Pizza | undefined {const pizzas = parse(jsonDbPath, defaultPizzas);return pizzas.find((pizza) => pizza.id === id);} // Fonction qui retourne un type Pizza ou undefined
Les classes
Les classes en TypeScript peuvent également être utilisées pour définir des types. Les classes peuvent être utilisées pour définir des types d'objets avec des propriétés et des méthodes.
Voici un exemple de classe pour définir un type de données :
tsclass Person {name: string;age: number;email?: string; // Propriété optionnelleconstructor(name: string, age: number, email?: string) {this.name = name;this.age = age;this.email = email;}}
👍 Dans ce cours, nous avons volontairement choisi de ne pas faire d'orienté objet en JS/TS. Nous vous recommandons de ne pas utiliser les classes pour définir des types, mais plutôt d'utiliser des interfaces ou des types.
Les enums
Les énumérations (enums) sont des types de données qui permettent de définir un ensemble de valeurs nommées. Les énumérations sont largement utilisées pour définir des types de données avec des valeurs prédéfinies.
Voici un exemple d'