TypeScript vs JavaScript : votre guide de référence
Publié: 2022-03-11TypeScript ou JavaScript ? Les développeurs envisagent ce choix pour les nouveaux projets Web ou Node.js, mais c'est une question qui mérite également d'être prise en compte pour les projets existants. Un sur-ensemble de JavaScript, TypeScript offre toutes les fonctionnalités de JavaScript ainsi que quelques avantages supplémentaires. TypeScript nous encourage intrinsèquement à coder proprement, ce qui rend le code plus évolutif. Cependant, les projets peuvent contenir autant de JavaScript brut que nous le souhaitons, donc l'utilisation de TypeScript n'est pas une proposition tout ou rien.
La relation entre TypeScript et JavaScript
TypeScript ajoute un système de type explicite à JavaScript, permettant une application stricte des types de variables. TypeScript exécute ses vérifications de type lors de la transpilation , une forme de compilation qui convertit le code TypeScript en code JavaScript que les navigateurs Web et Node.js comprennent.
Exemples TypeScript vs JavaScript
Commençons par un extrait de code JavaScript valide :
let var1 = "Hello"; var1 = 10; console.log(var1); Ici, var1 commence par une string , puis devient un number .
Étant donné que JavaScript n'est que faiblement typé, nous pouvons redéfinir var1 comme une variable de n'importe quel type, d'une chaîne à une fonction, à tout moment.
L'exécution de ce code produit 10 .
Maintenant, changeons ce code en TypeScript :
let var1: string = "Hello"; var1 = 10; console.log(var1); Dans ce cas, nous déclarons var1 comme étant une string . Nous essayons ensuite de lui attribuer un numéro, ce qui n'est pas autorisé par le système de type strict de TypeScript. La transpilation génère une erreur :
TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10; Si nous devions demander au transpileur de traiter l'extrait de code JavaScript d'origine comme s'il s'agissait de TypeScript, le transpileur déduirait automatiquement que var1 devrait être une string | number string | number . Il s'agit d'un type d'union TypeScript , qui nous permet d'attribuer à var1 une string ou un number à tout moment. Après avoir résolu le conflit de type, notre code TypeScript se transpilerait avec succès. Son exécution produirait le même résultat que l'exemple JavaScript.
TypeScript contre JavaScript à partir de 30 000 pieds : défis d'évolutivité
JavaScript est omniprésent, propulsant des projets de toutes tailles, appliqués de manière inimaginable à ses débuts dans les années 1990. Bien que JavaScript ait mûri, il est insuffisant en matière de prise en charge de l'évolutivité. En conséquence, les développeurs sont aux prises avec des applications JavaScript dont l'ampleur et la complexité ont augmenté.
Heureusement, TypeScript résout de nombreux problèmes de mise à l'échelle des projets JavaScript. Nous nous concentrerons sur les trois principaux défis : la validation, la refactorisation et la documentation.
Validation
Nous nous appuyons sur des environnements de développement intégrés (IDE) pour nous aider dans des tâches telles que l'ajout, la modification et le test de nouveau code, mais les IDE ne peuvent pas valider les références JavaScript pures. Nous atténuons cette lacune en surveillant attentivement pendant que nous codons pour éviter la possibilité de fautes de frappe dans les variables et les noms de fonction.
L'ampleur du problème augmente de manière exponentielle lorsque le code provient d'un tiers, où les références brisées dans les branches de code rarement exécutées pourraient facilement passer inaperçues.
En revanche, avec TypeScript, nous pouvons concentrer nos efforts sur le codage, sachant que toute erreur sera identifiée au moment de la transpilation. Pour le démontrer, commençons par du code JavaScript hérité :
const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } } L'appel .toISO() est une faute de frappe de la méthode moment.js toISOString() mais le code fonctionnerait, à condition que l'argument format ne soit pas ISO . La première fois que nous essayons de passer l' ISO à la fonction, cela déclenchera cette erreur d'exécution : TypeError: moment(...).toISO is not a function .
Localiser le code mal orthographié peut être difficile. La base de code actuelle peut ne pas avoir de chemin vers la ligne brisée, auquel cas notre référence .toISO() brisée ne serait pas détectée par les tests.
Si nous portons ce code vers TypeScript, l'IDE mettrait en évidence la référence cassée, nous invitant à apporter des corrections. Si nous ne faisions rien et tentions de transpiler, nous serions bloqués et le transpileur générerait l'erreur suivante :
TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());Refactoring
Bien que les fautes de frappe dans les références de code tierces ne soient pas rares, il existe un ensemble différent de problèmes associés aux fautes de frappe dans les références internes, comme celui-ci :
const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); } Un seul développeur peut localiser et réparer toutes les instances de phoneNumbr pour qu'elles se terminent par er assez facilement.
Mais plus l'équipe est grande, plus cette erreur simple et courante est déraisonnablement coûteuse. Au cours de l'exécution de leur travail, les collègues devraient être conscients de ces fautes de frappe et les propager. Alternativement, ajouter du code pour prendre en charge les deux orthographes gonflerait inutilement la base de code.
Avec TypeScript, lorsque nous corrigeons une faute de frappe, le code dépendant ne se transpilera plus, signalant aux collègues de propager le correctif à leur code.
Documentation
Une documentation précise et pertinente est essentielle à la communication au sein et entre les équipes de développeurs. Les développeurs JavaScript utilisent souvent JSDoc pour documenter les types de méthodes et de propriétés attendus.
Les fonctionnalités du langage TypeScript (par exemple, les classes abstraites, les interfaces et les définitions de type) facilitent la programmation de conception par contrat, conduisant à une documentation de qualité. De plus, avoir une définition formelle des méthodes et des propriétés auxquelles un objet doit adhérer aide à identifier les changements avec rupture, à créer des tests, à effectuer une introspection du code et à implémenter des modèles architecturaux.
Pour TypeScript, l'outil de référence TypeDoc (basé sur la proposition TSDoc) extrait automatiquement les informations de type (par exemple, classe, interface, méthode et propriété) de notre code. Ainsi, nous créons sans effort une documentation qui est de loin plus complète que celle de JSDoc.
Avantages de TypeScript par rapport à JavaScript
Voyons maintenant comment nous pouvons utiliser TypeScript pour relever ces défis d'évolutivité.
Suggestions avancées de code/refactoring
De nombreux IDE peuvent traiter les informations du système de type TypeScript, fournissant une validation de référence au fur et à mesure que nous codons. Mieux encore, au fur et à mesure que nous tapons, l'EDI peut fournir une documentation pertinente en un coup d'œil (par exemple, les arguments qu'une fonction attend) pour toute référence et suggérer des noms de variables contextuellement corrects.
Dans cet extrait TypeScript, l'IDE suggère une saisie semi-automatique des noms des clés dans la valeur de retour de la fonction :
/** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }Mon IDE, Visual Studio Code, a fourni cette suggestion (dans la légende) lorsque j'ai commencé à appeler la fonction (ligne 31):
De plus, les suggestions de saisie semi-automatique de l'IDE (dans la légende) sont contextuellement correctes, affichant uniquement les noms valides dans une situation de clé imbriquée (ligne 34) :
De telles suggestions en temps réel conduisent à un codage plus rapide. De plus, les IDE peuvent s'appuyer sur les informations de type rigoureuses de TypeScript pour refactoriser le code à n'importe quelle échelle. Les opérations telles que renommer une propriété, modifier l'emplacement des fichiers ou même extraire une superclasse deviennent triviales lorsque nous sommes sûrs à 100 % de l'exactitude de nos références.

Prise en charge des interfaces
Contrairement à JavaScript, TypeScript offre la possibilité de définir des types à l'aide d' interfaces . Une interface répertorie formellement, mais n'implémente pas, les méthodes et propriétés qu'un objet doit inclure. Cette construction de langage est particulièrement utile pour la collaboration avec d'autres développeurs.
L'exemple suivant montre comment nous pouvons tirer parti des fonctionnalités de TypeScript pour implémenter proprement des modèles OOP courants - dans ce cas, stratégie et chaîne de responsabilité - améliorant ainsi l'exemple précédent :
export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }Modules ES6—partout
Au moment d'écrire ces lignes, tous les runtimes JavaScript frontaux et principaux ne prennent pas en charge les modules ES6. Avec TypeScript, cependant, nous pouvons utiliser la syntaxe du module ES6 :
import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c'])); La sortie transpilée sera compatible avec notre environnement sélectionné. Par exemple, en utilisant l'option du compilateur --module CommonJS , nous obtenons :
"use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; En utilisant --module UMD à la place, TypeScript génère le modèle UMD plus détaillé :
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });Cours ES6—Partout
Les environnements hérités ne prennent souvent pas en charge les classes ES6. Un transpile TypeScript garantit la compatibilité en utilisant des constructions spécifiques à la cible. Voici un extrait de source TypeScript :
export class TestClass { hello = 'World'; }La sortie JavaScript dépend à la fois du module et de la cible, que TypeScript nous permet de spécifier.
Voici ce que --module CommonJS --target es3 donne :
"use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass; En utilisant --module CommonJS --target es6 à la place, nous obtenons le résultat transpilé suivant. Le mot-clé class est utilisé pour cibler ES6 :
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;Fonctionnalité asynchrone/en attente, où que vous soyez
Async/wait facilite la compréhension et la maintenance du code JavaScript asynchrone. TypeScript offre cette fonctionnalité à tous les runtimes, même à ceux qui ne fournissent pas async/wait nativement.
Notez que pour exécuter async/wait sur des runtimes plus anciens comme ES3 et ES5, vous aurez besoin d'un support externe pour la sortie basée sur Promise (par exemple, via Bluebird ou un polyfill ES2015). Le polyfill Promise fourni avec TypeScript s'intègre facilement dans la sortie transpilée. Il nous suffit de configurer l'option du compilateur lib en conséquence.
Prise en charge des champs de classe privée, partout
Même pour les cibles héritées, TypeScript prend en charge les champs private de la même manière que les langages fortement typés (par exemple, Java ou C#). En revanche, de nombreux runtimes JavaScript prennent en charge les champs private via la syntaxe de préfixe de hachage, qui est une proposition finie d'ES2022.
Inconvénients de TypeScript par rapport à JavaScript
Maintenant que nous avons mis en évidence les principaux avantages de la mise en œuvre de TypeScript, explorons les scénarios où TypeScript peut ne pas être la bonne solution.
Transpilation : potentiel d'incompatibilité du flux de travail
Des flux de travail ou des exigences de projet spécifiques peuvent être incompatibles avec l'étape de transpilation de TypeScript : par exemple, si nous devions utiliser un outil externe pour modifier le code après le déploiement ou si la sortie générée doit être conviviale pour les développeurs.
Par exemple, j'ai récemment écrit une fonction AWS Lambda pour un environnement Node.js. TypeScript ne convenait pas, car exiger une transpilation m'empêcherait, ainsi que les autres membres de l'équipe, de modifier la fonction à l'aide de l'éditeur en ligne AWS. C'était une rupture pour le chef de projet.
Le système de type ne fonctionne que jusqu'au moment de la transpilation
La sortie JavaScript de TypeScript ne contient pas d'informations de type, elle n'effectuera donc pas de vérifications de type et, par conséquent, la sécurité du type peut être interrompue au moment de l'exécution. Par exemple, supposons qu'une fonction soit définie pour toujours renvoyer un objet. Si null est renvoyé à partir de son utilisation dans un fichier .js , une erreur d'exécution se produit.
Les fonctionnalités dépendantes des informations de type (par exemple, les champs privés, les interfaces ou les génériques) ajoutent de la valeur à tout projet, mais sont supprimées lors de la transpilation. Par exemple, les membres de la classe private ne seraient plus privés après la transpilation. Pour être clair, les problèmes d'exécution de cette nature ne sont pas propres à TypeScript, et vous pouvez également vous attendre à rencontrer les mêmes difficultés avec JavaScript.
Combiner TypeScript et JavaScript
Malgré les nombreux avantages de TypeScript, nous ne pouvons parfois pas justifier la conversion d'un projet JavaScript entier en une seule fois. Heureusement, nous pouvons spécifier au transpileur TypeScript, fichier par fichier, ce qu'il faut interpréter comme du JavaScript simple. En fait, cette approche hybride peut aider à atténuer les défis individuels à mesure qu'ils surviennent au cours du cycle de vie d'un projet.
Nous pouvons préférer laisser JavaScript inchangé si le code :
- A été écrit par un ancien collègue et nécessiterait d'importants efforts de rétro-ingénierie pour être converti en TypeScript.
- Utilise des techniques non autorisées dans TypeScript (par exemple, ajoute une propriété après l'instanciation d'un objet) et nécessiterait une refactorisation pour respecter les règles de TypeScript.
- Appartient à une autre équipe qui continue d'utiliser JavaScript.
Dans de tels cas, un fichier de déclaration (fichier .d.ts , parfois appelé fichier de définition ou fichier de typage) donne à TypeScript suffisamment de données de type pour activer les suggestions IDE tout en laissant le code JavaScript tel quel.
De nombreuses bibliothèques JavaScript (par exemple, Lodash, Jest et React) fournissent des fichiers de typage TypeScript dans des packages de types séparés, tandis que d'autres (par exemple, Moment.js, Axios et Luxon) intègrent des fichiers de typage dans le package principal.
TypeScript vs JavaScript : une question de rationalisation et d'évolutivité
La prise en charge, la flexibilité et les améliorations inégalées disponibles via TypeScript améliorent considérablement l'expérience des développeurs, permettant aux projets et aux équipes d'évoluer. Le coût principal de l'incorporation de TypeScript dans un projet est l'ajout de l'étape de construction de transpilation. Pour la plupart des applications, la transpilation en JavaScript n'est pas un problème ; c'est plutôt un tremplin vers les nombreux avantages de TypeScript.
Lectures complémentaires sur le blog Toptal Engineering :
- Utilisation de TypeScript et de la prise en charge de Jest : un didacticiel AWS SAM
