Blog ENI : Toute la veille numérique !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
💥 Du 22 au 24 novembre : Accès 100% GRATUIT
à la Bibliothèque Numérique ENI. Je m'inscris !

Les modules

Introduction

Une variable déclarée à l’extérieur de tout bloc de code a une portée globale. Il en est de même pour les fonctions et les classes. Cette portée globale leur permet d’être accessibles à travers l’intégralité du programme, mais pose un problème si une autre déclaration (variable, fonction ou classe) utilise le même nom. Ce phénomène, appelé collision de nom, peut entraîner des erreurs ou des comportements inattendus lors de l’exécution du programme. Le compilateur de TypeScript détecte ces collisions et lève une erreur lors de la compilation.

Exemple :

// Compilation error TS2393: 
// Duplicate function implementation. 
function getSalary() { 
  return 10000; 
} 
 
// Compilation error TS2393: 
// Duplicate function implementation. 
function getSalary() { 
  return 10; 
} 
 
console.log(getSalary()); 

Dans cet exemple, si le code est exécuté, alors la dernière ligne affichera 10 dans la console. En effet, la seconde déclaration de la fonction getSalary() écrase la précédente.

Avoir des déclarations qui ont une portée globale est donc une très mauvaise pratique qui entrave considérablement l’évolutivité...

Historique

1. Pattern module

Entre 2005 et 2010, AJAX a été standardisé et de nombreuses librairies JavaScript étendant les capacités du langage apparaissent : jQuery, Dojo, Prototype… Les applications web utilisent dès lors de plus en plus de code JavaScript afin de dynamiser les pages, mais les collisions de noms posent un véritable problème. Le pattern module est la première solution permettant d’encapsuler et d’isoler les déclarations. Adopté par la plupart des librairies de l’époque, il se base principalement sur la capacité des closures (cf. chapitre Types et instructions basiques - Fonctions) et possède différentes implémentations.

Pour créer un module, il faut utiliser une Immediately Invoked Function Expression (expression de fonction invoquée immédiatement en français) plus connue sous l’alias IIFE. Une IIFE est une fonction anonyme affectée à une variable, immédiatement exécutée.

Exemple :

const salariedModule = (function() { 
  const salary = 20_000; 
 
  return { 
    getSalary: function() { 
      return salary; 
    } 
  }; 
})(); 
 
const employeeModule = (function(salariedModule) { 
  return { 
    personList: ["Evelyn", "John"], 
    getSalary: function() { 
      return salariedModule.getSalary(); 
    } 
 }; 
})(salariedModule); 
 
const managerModule = (function(salariedModule) { 
  const bonus = 40_000; 
  return { 
    personList: ["Patrick", "David"], 
    getSalary: function() { 
      return salariedModule.getSalary() + bonus; 
    } 
  }; 
})(salariedModule); 
 
// Log: 20 000 
console.log(employeeModule.getSalary()); 
 
// Log: 60 000 
console.log(managerModule.getSalary()); 
 
// Log: ["Evelyn", "John"] 
console.log(employeeModule.personList); 
 
// Log: ["Patrick", "David"] ...

La norme ECMAScript 2015

Les modules définis par la norme ECMAScript 2015 tirent le meilleur parti des normes CommonJS et AMD. Comme pour CommonJS, la norme propose une syntaxe simple permettant d’exporter et importer les données d’un module via deux mots-clés :

  • export : permet d’exporter des éléments.

  • import : permet d’importer des éléments depuis un module.

Dès lors qu’un fichier possède l’un de ces mots-clés, il devient un module. Il ne peut donc y avoir qu’un seul module par fichier. Les modules ECMAScript 2015 visent à supporter le chargement asynchrone et synchrone. Ils permettent aussi de gérer les dépendances cycliques (bien que cela soit considéré comme une mauvaise pratique).

Les modules doivent être importés en dehors de tout bloc de code. Toutefois, il est possible de les importer dynamiquement via l’utilisation de la fonction import(), qui retourne le module sous la forme d’une promesse (le concept de promesse sera abordé dans le chapitre Asynchronisme). Cette fonctionnalité a été standardisée dans la version 2020 d’ECMAScript.

Exemple (premier module) :

/** 
 * salariedModule.js 
 */ 
const salary = 20_000; 
 
export const salaried = { 
  getSalary: function() { 
    return salary; 
  } 
}; 

Exemple (second module) :

/** 
 * employeeModule.js 
 */ 
import { salaried } from "./salariedModule"; 
 
export const employee = { 
  personList: ["Patrick", "David"], 
  getSalary: function() { 
    return salaried.getSalary(); 
  } 
}; 

Exemple (troisième module) :

/** 
 * managerModule.js 
 */ 
import { salaried } from "./salariedModule"; 
 
const bonus = 40_000; 
 
export const manager = { 
  personList: ["Patrick", "David"], 
  getSalary: function() { 
    return salaried.getSalary() + bonus; 
  } 
}; 

Exemple (utilisation des trois modules) :

/** 
 * main.js 
 */ 
import { employee } from "./employeeModule"; ...

Gestion des types

Import type

Comme dit précédemment, les types définis en TypeScript ne sont utiles qu’à la compilation et sont supprimés lors de la transpilation en code JavaScript.

Exemple (export) :

/**   
 * type.js   
 */   
export interface Employee {  
  // ...  
} 

Exemple (import) :

/**  
 * main.ts  
 */  
import { Employee } from "./type"; 
 
const evelyn: Employee = { 
 // ... 
}; 

Dans cet exemple, le fichier type.ts disparaîtra lors de la transpilation, ainsi que la ligne d’import du fichier main.ts. Il existe une notation qui permet d’aider le compilateur et de rendre ce comportement plus explicite : l’import et export de type.

Syntaxe (export) :

export type { element1, element2, ... } from "./path/to/module"; 

Exemple (export) :

// type.ts 
export type interface Employee { 
  // ... 
} 

Syntaxe (import) :

import type { element1, element2, ... } from "./path/to/module"; 

Exemple (import) :

// main.ts 
import type { Employee } from "./type"; 
 
const evelyn: Employee = { 
 // ... 
}; 

L’option de compilation --importsNotUsedAsValues permet de définir le comportement du mot-clé import uniquement...

Chargement et résolutions

1. Résolutions

La plupart des projets récents utilisent le concept de module. Lors d’un import de module, le fichier représentant le module doit être résolu au moment de la compilation par le compilateur de TypeScript. Il en est de même lors de l’exécution du code par le moteur JavaScript. TypeScript va tenter de résoudre ces imports à partir d’un algorithme qui peut être sélectionné via une option de compilation nommée --moduleResolution. Une fois résolu, le compilateur peut proposer :

  • L’autocomplétion lors de l’import du module.

  • La détection d’erreur.

  • L’autocomplétion lors de l’utilisation du module.

L’option de compilation --traceResolution permet d’obtenir des logs lorsque le compilateur de TypeScript tente de résoudre les différents imports. Cela s’avère utile pour diagnostiquer un import que le compilateur n’arrive pas à résoudre.

Il existe deux manières de définir le chemin d’accès à un module lors de l’import :

  • Les imports ayant des chemins relatifs sont des modules internes au projet. Grâce à ce chemin, le compilateur de TypeScript va essayer de retrouver le fichier ayant pour extension .ts ou .d.ts correspondant à cet import. S’il n’est pas trouvé, une erreur de compilation sera levée.

Exemple :

import { employee } from "./employeeModule"; 
  • Les chemins d’import uniquement composés d’un nom sont des modules provenant d’un package NPM et donc existant dans le dossier nodes_modules (celui-ci contient l’intégralité des packages). Pour résoudre cet import, le compilateur va chercher dans le dossier nodes_module le nom du dossier correspondant au nom précisé...