Importer des modules externes en Typescript

Le but de cet article est d’illustrer l’import de bibliothèques externes Javascript dans du code Typescript. Il fait suite à un article précédent qui expliquait comment on pouvait séparer le code Typescript en modules (cf. Les modules en Typescript en 5 min).

Le compilateur Typescript permet de générer du code Javascript exécutable sur tous les browsers. Un des gros intérêts d’implémenter en Typescript est d’abord d’avoir un typage fort et ensuite de permettre au compilateur d’effectuer une vérification syntaxique. Le code Javascript généré est ainsi plus robuste puisque une vérification des types a déjà été effectuée à la compilation.

L’écosystème Javascript est très riche en bibliothèques. Ces bibliothèques sont couramment utilisées pour enrichir une application Javascript. Toutefois la plupart d’entre elles ne sont pas implémentées en Typescript. Heureusement il existe quelques méthodes pour utiliser ces bibliothèques Javascript et continuer à tirer partie des avantages de la compilation Typescript. Dans cet article, on va présenter quelques méthodes les plus courantes.


Dans un premier temps, on va indiquer la solution utilisée pour permettre l’import de bibliothèques externes au niveau de la syntaxe Typescript. Dans un 2e temps, on indiquera les méthodes les plus courantes pour mettre en pratique cette solution.

Préambule

Avant de rentrer dans les détails, on va expliquer comment il est possible d’importer du code Javascript dans du code Typescript.

Mot-clé “declare”

Comme on l’a indiqué plus haut, le compilateur Typescript effectue une vérification des types des objets. Le type de tous les objets ainsi que celui les dépendances sont vérifiés, il devient alors plus compliqué d’importer du code Javascript qui n’est pas obligatoirement fortement typé. Pour palier à ce problème, le mot-clé declare permet de déclarer des variables ne provenant pas de code Typescript. Il donne ainsi la possiblité d’introduire dans du code Typescript, des types provenant de code Javascript. Le compilateur n’ira pas effectuer des vérifications dans le code Javascript, toutefois il prendra en compte le type déclaré pour vérifier le code Typescript.

Par exemple si on écrit:

declare var externalLibrary; 

On peut introduire une variable nommée externalLibrary de type any qui pourra être utilisée dans le code Typescript comme si on l’avait déclaré de cette façon:

var externalLibrary: any; 

Le code Javascript généré est le même, toutefois utiliser declare permet d’indiquer qu’il s’agit de l’import d’un objet défini de façon externe.

Il faut avoir à l’esprit que declare permet seulement d’indiquer au compilateur le type d’une variable sans explicitement indiquer l’implémentation. Il part du principe que l’implémentation Javascript correspondante devra être fournie à l’exécution.
Ainsi en fonction des déclarations de types indiquées avec declare, le compilateur ne fera que vérifier la syntaxe Typescript, il ne “transpile” en Javascript que le code Typescript. Si l’implémentation Javascript correspondant aux types déclarés n’est pas présente, l’exécution provoquera des erreurs.

Le mot-clé declare permet de déclarer tous les types d’objets comme le ferait, par exemple, une interface par rapport à une classe.

Pour déclarer une variable de type any:

declare var variableName: any; 

Pour déclarer la signature d’une fonction:

declare function DecodeValue(valueName: string): void; 

Pour déclarer une classe:

declare class Person { 
     constructor(name: string, firstName: string); 
     showPersonName(): void; 
} 

Pour déclarer un module ou un namespace:

declare namespace ExternalDependency { 
    class Person { 
         constructor(name: string, firstName: string); 
         showPersonName(): void; 
    } 

    class Player { 
         constructor(person: Person); 
         showPlayerName(): void; 
    } 

    function CreatePlayer(name: string, firstName: string): Player; 
} 
module et namespace sont équivalents

Les mot-clés module et namespace sont équivalents (cf. Namespaces en Typescript), on peut aussi écrire:

declare module ExternalDependency { 
   ... 
} 

On peut associer les mot-clé export et declare pour indiquer l’export d’un module et de tous les éléments qui s’y trouvent.

Ainsi:

export declare namespace ExternalDependency { 
    class Person { 
         constructor(name: string, firstName: string); 
         showPersonName(): void; 
    } 

    class Player { 
         constructor(person: Person); 
         showPlayerName(): void; 
    } 

    function CreatePlayer(name: string, firstName: string): Player; 
} 

Est équivalent à:

export namespace ExternalDependency { 
    export class Person { 
         constructor(name: string, firstName: string); 
         showPersonName(): void; 
    } 
 
    export class Player { 
         constructor(person: Person); 
         showPlayerName(): void; 
    } 

    export function CreatePlayer(name: string, firstName: string): Player; 
} 

Fichier de définition (declaration file)

Dans le cas d’une bibliothèque, tous les types Javascript peuvent être déclarés dans un seul fichier appelé “fichier de définition” (i.e. declaration file). Ces fichiers ont usuellement l’extension .d.ts toutefois rien n’oblige à utiliser cette extension. N’importe quelle déclaration peut être implémentée dans un fichier .ts.

Les fichiers de définition .d.ts peuvent être référencés avec une directive triple-slash (cf. Directive “triple-slash”) au même titre qu’un fichier .ts normal, par exemple:

/// <reference path="declarationFile.d.ts" />  

Ainsi quand on doit utiliser une bibliothèque Javascript dans du code Typescript, on peut référencer les fichiers de définition correspondant à cette bibliothèque.

Générer un fichier de définition

Le compilateur Typescript permet de générer un fichier de définition à partir du code Typescript en écrivant:

tsc --declaration <chemin des fichiers .ts>

Ou

tsc –d <chemin des fichiers .ts>

On peut préciser un répertoire de sortie pour ces fichiers:

tsc --declaration <chemin des fichiers .ts> --declarationDir <répertoire de sortie>

Exemple d’utilisation de “declare”

On se propose d’illustrer l’import d’un fichier de définition avec un exemple. Le but de cet exemple est d’importer du code Javascript dans du code Typescript en utilisant un fichier de définition. Le code de la bibliothèque se trouve dans les fichiers dependency.ts ou dependency.js.

Code sur GitHub

Le code de cet article se trouve dans le repository GitHub suivant:
github.com/msoft/external_typescript_modules.
Le code se trouve dans des branches différentes suivant la partie de l’article qu’il illustre:

  • Branche 1_initial: code initial permettant de compiler du code Typescript.
  • Branche 2_ExternalDependency: exemple d’utilisation de declare.
  • Branche 3_webpack_initial: code initial permettant de compiler avec webpack.
  • Branche 4_webpack_any: exemple d’import de modules Javascript avec any.
  • Branche 5_webpack_npm_types: exemple d’import de modules Javascript avec le domaine @types de npm.
  • Branche 6_webpack_typings: exemple d’import de modules Javascript avec typings.

Le code de cet exemple se trouve dans la branche 1_initial du repository msoft/external_typescript_modules sur GitHub.

Ainsi on considère le code suivant permettant d’importer un module:

import { Person, Player, CreatePlayer } from "./dependency.js";

class Startup { 
    public static main(): number { 
        var player = new Player(new Person('Buffon', 'Gianluigi')); 
        player.showPlayerName(); 

        return 0; 
    } 
} 

Startup.main(); 

Le module se trouve dans le fichier dependency.ts (la directive d’import utilise l’extension .js dans le code Typescript car le compilateur ne change pas les extensions à la compilation(1) (2)).

Le fichier dependency.ts contient le code suivant:

export class Person { 
    constructor(private name: string, private firstName: string) { 
    } 

    public showPersonName(): void { 
        console.log("Name: " + this.name + "; First Name: " + this.firstName);  
    }
}
 
export class Player { 
    constructor(private person: Person) { 
    } 

    public showPlayerName(): void { 
        this.person.showPersonName();  
    } 
} 

export function CreatePlayer(name: string, firstName: string): Player { 
    return new Player(new Person(name, firstName)); 
} 

Le fichier public/index.html qui va permettre de lancer l’exécution (il ne fait que déclarer les fichiers Javascript contenant le code) est:

<!DOCTYPE html> 
<html lang="en"> 
    <head> 
        <meta charset="UTF-8">  
        <title>External module import</title> 
    </head>    
    <body> 
        <script type="module" src="dependency.js"    ></script>
        <script type="module" src="index.js"    ></script>
    </body> 
</html> 

Pour exécuter cet exemple, il faut:

  1. Cloner le repository en exécutant les instructions:
    ~% git clone https://github.com/msoft/external_typescript_modules.git
    ~/external_typescript_modules/% cd external_typescript_modules
    ~/external_typescript_modules/% git checkout 2_ExternalDependency
    
  2. Compiler en exécutant les instructions suivantes dans le répertoire de l’exemple:
    ~/external_typescript_modules/% npm install 
    ~/external_typescript_modules/% npm run build 
    
  3. Lancer l’exécution ensuite avec:
    ~/external_typescript_modules/% npm start 
    
  4. Ouvrir un browser à l’adresse http://127.0.0.1:8080 puis ouvrir la console de développement.
Pour afficher la console de développement dans un browser

Pour tous les exemples présentés dans cet article, pour voir les résultats d’exécution, il faut afficher la console de développement:

  • Sous Firefox: on peut utiliser la raccourci [Ctrl] + [Maj] + [J] (sous MacOS: [⌘] + [Maj] + [J], sous Linux: [Ctrl] + [Maj] + [K]) ou en allant dans le menu “Développement web” ⇒ “Console du navigateur”.
  • Sous Chrome: utiliser le raccourci [F12] (sous MacOS: [⌥] + [⌘] + [I], sous Linux: [Ctrl] + [Maj] + [I]) puis cliquer sur l’onglet “Console”. A partir du menu, il faut aller dans “Plus d’outils” ⇒ “Outils de développement”.
  • Sous EDGE: utiliser le raccourci [F12] puis naviguer jusqu’à l’onglet “Console”.

Le résultat de l’exécution est du type:

Name: Buffon; First Name: Gianluigi 

On modifie le code de dependency.ts pour qu’il ne contienne que des déclarations de types (on supprime toutes les implémentations):

  1. On modifie le code de cette façon:
    declare namespace ExternalDependency { 
            class Person { 
                constructor(name: string, firstName: string); 
                showPersonName(): void;  
            } 
    
            class Player { 
                constructor(person: Person); 
                showPlayerName(): void; 
            } 
    
            function CreatePlayer(name: string, firstName: string): Player;     
    } 
    
  2. On déplace ensuite ce fichier dans le répertoire ExternalDependency/index.d.ts:
    ~/external_typescript_modules/% mkdir ExternalDependency 
    ~/external_typescript_modules/% mv dependency.ts ExternalDependency/index.d.ts 
    
  3. On importe ensuite ce fichier dans index.ts en utilisant une directive triple-slash et en supprimant la directive d’import de module:
    /// <reference path="ExternalDependency/index.d.ts" />
    
    class Startup { 
        public static main(): number { 
            var player = ExternalDependency.CreatePlayer('Buffon', 'Gianluigi'); 
            player.showPlayerName(); 
    
            return 0; 
        } 
    } 
    
    Startup.main(); 
    
  4. Si on compile, on s’aperçoit qu’il n’y a pas d’erreurs de compilation. Il n’existe plus d’implémentation des classes Player et Person pourtant la compilation se passe correctement.
  5. En revanche si on tente d’exécuter le code en rafraichissant le browser:
    index.js:24 Uncaught ReferenceError: ExternalDependency is not defined 
        at Function.Startup.main (index.js:24) 
        at index.js:30 
    

    Ceci s’explique par le fait, qu’il n’y a pas de références vers le code Javascript correspondant aux classes Player et Person.

  6. On modifie le code de dependency.ts pour encapsuler les classes Player et Person dans un module et on exporte seulement la fonction CreatePlayer():
    module ExternalDependency { 
        class Person { 
            constructor(private name: string, private firstName: string) { 
            } 
    
            public showPersonName(): void { 
                console.log("Name: " + this.name + "; First Name: " + this.firstName);  
            } 
        }
    
        class Player { 
            constructor(private person: Person) { 
            }
    
            public showPlayerName(): void { 
                this.person.showPersonName();  
            } 
        } 
    
        export function CreatePlayer(name: string, firstName: string): Player { 
            return new Player(new Person(name, firstName)); 
        } 
    } 
    
  7. On lance la compilation en exécutant:
    npm run build 
    
  8. Pour rajouter une référence vers le code Javascript des classes Player et Person, on déplace au bon endroit le fichier Javascript dependency.js compilé précédemment:
    ~/external_typescript_modules/% mkdir public/ExternalDependency 
    ~/external_typescript_modules/% mv public/dependency.js public/ExternalDependency/index.js 
    
  9. On modifie ensuite le fichier public/index.html pour qu’il ne référence plus les fichiers Javascript sous forme de module ES6 (cf. Utilisation des modules ES2015):
    <!DOCTYPE html> 
    <html lang="en"> 
        <head> 
            <meta charset="UTF-8">  
            <title>External module import</title> 
        </head>    
        <body> 
            <script src="ExternalDependency/index.js"    ></script> 
            <script src="index.js"    ></script>
        </body> 
    </html> 
    
  10. Après avoir rafraîchi le browser, le résultat est le même que précédemment:
    Name: Buffon; First Name: Gianluigi 
    

Le but de cet exemple était d’illustrer l’utilisation d’un fichier de définition de façon à comprendre plus facilement leur utilisation par la suite.

Comment utiliser des fichiers de définition ?

Comme indiqué plus haut, ces fichiers servent à déclarer des types sans préciser l’implémentation. En effet l’implémentation de ces types est en Javascript et sera utilisable seulement pendant l’exécution. Les fichiers de définition contiennent le code Typescript permettant au compilateur de faire une vérification des types.

Ainsi la plupart des bibliothèques Javascript téléchargeables sous forme de modules avec npm possèdent des fichiers de définition. Il existe plusieurs façon d’obtenir ces fichiers. On va indiquer 2 méthodes pour télécharger ces fichiers.

Exemple avec jQuery et DataTables

Pour illustrer ces différentes méthodes, on se propose d’utiliser un exemple dans lequel on utilise jQuery et DataTables. L’exemple permet de remplir un tableau avec 2 lignes. En dessous du tableau se trouve un bouton. Si on clique sur ce bouton, le contenu de la cellule à la 2e ligne et 2e colonne est modifié.

Pour utiliser l’exemple:

  1. On commence à partir d’un “squelette” vide ne contenant que webpack. Webpack est un outil permettant de compiler le code Typescript dans un seul fichier Javascript (appelé bundle). Webpack permet aussi d’exécuter le code en utilisant un serveur web de développement.

    Le “squelette” initial se trouve dans le branche 3_webpack_initial du repository GitHub msoft/external_typescript_modules.
    Le code final de cet exemple se trouve dans la branche 4_webpack_any.

    On récupère la branche 3_webpack_initial en exécutant:

    ~/external_typescript_modules/% git checkout 3_webpack_initial
    
  2. On installe tous les composants y compris webpack en exécutant la ligne suivante:
    ~/external_typescript_modules/% npm install 
    
  3. On ajoute les bibliothèques jQuery et DataTables en exécutant:
    ~/external_typescript_modules/% npm install jquery 
    ~/external_typescript_modules/% npm install datatables.net 
    
  4. A ce stade, si on tente d’utiliser du code jQuery dans index.ts, le code ne compilera pas:
    var data = [ 
                [ 
                    "Tiger Nixon", 
                    "System Architect", 
                    "Edinburgh", 
                    "5421", 
                    "2011/04/25", 
                    "$3,120" 
                ], 
                [ 
                    "Garrett Winters", 
                    "Director", 
                    "Edinburgh", 
                    "8422", 
                    "2011/07/25", 
                    "$5,300" 
                ] 
            ] 
    
            $(document).ready( function () { 
                var datatable = $('#table_id').DataTable({ 
                    data,
                }); 
            } ); 
    

Dans le code ci-dessus, jQuery est appelé avec l’instruction $(...) et le code de DataTables est appelé avec .DataTable(...).
Les instructions permettent de rajouter des données dans le tableau HTML nommé table_id se trouvant dans la page HTML index.html.

En lançant npm run build pour compiler, on obtient des erreurs de compilation:

ERROR in /home/user/external_typescript_modules/webpack_es6/index.ts 
./index.ts 
[tsl] ERROR in /home/user/external_typescript_modules/webpack_es6/index.ts(9,15) 
      TS2451: Cannot redeclare block-scoped variable '$'. 
ℹ 「wdm」: Failed to compile. 

Les méthodes suivantes permettent de corriger ces erreurs.

Utiliser “any”

Cette méthode est la plus simple toutefois elle est la plus risquée. Elle consiste à déclarer le type any pour l’objet de plus haut niveau dans la bibliothèque, par exemple en indiquant dans le fichier index.ts:

declare const $: any; 

L’erreur de compilation disparaît, toutefois il faut avoir en tête qu’il n’y aucune vérification de syntaxe sur toutes les déclarations suivant $. On peut écrire n’importe quoi après $, il n’y aura pas d’erreurs de compilation. Cette solution est donc à utiliser pour tester rapidement une bibliothèque mais elle est à proscrire pour produire du code de production.

Installer les fichiers de définition avec npm

Le domaine types de la commande npm permet d’installer les fichiers de définition dans le répertoire node_modules/@types/<nom du package>. Pour installer ces fichiers, on peut exécuter la commande:

npm install @types/<nom du package> --save-dev 

Cette commande va installer les fichiers de définition et indique ce package dans la partie devDependencies du fichier package.json.

Dans la plupart des cas pour télécharger les fichiers de définition pour un package donné, il suffit d’installer le package avec le nom @types/<nom du package>. Ce n’est pas toujours le cas, ainsi pour retrouver le package contenant les fichiers de définition pour un package donné, on peut utiliser TypeSearch.

Il faut privilégier l’installation de fichiers de définition avec npm

Cette méthode est actuellement la méthode la plus usuelle pour télécharger les fichiers de définition. Cette fonctionnalité n’est disponible qu’à partir de Typescript 2.0.

Les fichiers de définition dans le domaine @types de npm proviennent du repository GitHub DefinitelyTyped qui est un référentiel contenant les fichiers de définition pour les packages les plus courants.

Dans le cas de notre exemple, on peut installer les fichiers de définition de jQuery et DataTables en exécutant:

~/external_typescript_modules/% npm install @types/jquery --save-dev 
~/external_typescript_modules/% npm install @types/datatables.net --save-dev 

On peut remarquer que la compilation réussit après l’installation des fichiers de définition (après avoir exécuté npm run build).

Le code final de cet exemple se trouve dans la branche 5_webpack_npm_types du repository GitHub msoft/external_typescript_modules.

Installer les fichiers de définition avec typings

Pour des versions de Typescript antérieures à la version 2.0 ou pour obtenir des fichiers de définition pour des packages qui ne sont pas disponibles dans le domaine @types de npm, on peut passer par typings.

typings est un outil disponible avec la ligne de commandes qui possède de nombreuses fonctionnalités pour télécharger les fichiers de définition à partir de sources différentes.

Import sous forme de module externe

Par défaut typings considère que l’import des types de définition se fait sous forme de module externe Typescript. Typings encapsule, ensuite la définition des types dans un module avec des déclarations du type:

declare module '<nom du module>' { 
     // Définition du type 
     // ... 
} 

Pour consommer le type dans le code Typescript, on peut utiliser des alias en utilisation la syntaxe:

import * as <alias utilisé> from '<nom du module>' 

Un des intérêts de cette méthode est de pouvoir utiliser des versions différentes de fichiers de définition correspondant à des versions différentes de package. On peut, ainsi, utiliser un alias par version.

Dépendances globales

Typings considère certaines définitions de types comme étant globales pour différentes raisons:

  • Soit parce-qu’il ajoute les types au scope global,
  • Soit il ajoute des éléments nécessaires pour effectuer les builds (comme webpack ou browserify)
  • Soit il ajoute des éléments nécessaires à l’exécution (par exemple comme Node.js).

Pour installer des définitions globales, il faut utiliser l’option --global. Typings indique si l’installation de types doit se faire obligatoirement de façon globale.

Utiliser typings

Pour installer typings, on utilise npm en exécutant la commande:

npm install typings --global  

Cette commande ajoute l’utilitaire typings dans le répertoire global de façon à ce qu’elle soit disponible sur la ligne de commandes.

Quelques commandes courantes de typings

Une fois que typings est installé, on peut l’utiliser directement à la ligne de commandes:

  • Pour chercher des définitions correspondant à un package à partir de son nom, on peut taper:
    typings search --name <nom du package> 
    
  • Pour chercher en fonction d’un mot clé:
    typings search <éléments recherchés> 
    
  • Pour installer des définitions à partir du nom du package:
    typings install <nom du package> --save 
    

    L’option --save permet d’enregistrer le nom du package pour lequel les définitions ont été téléchargées dans le fichier typings.json dans le nœud json:

    • "dependencies" pour les packages externes classiques
    • "globalDependencies" pour les packages installés de façon globale.
  • Pour installer les définitions de façon globale, il faut rajouter l’option --global:
    typings install <nom du package> --save --global 
    
  • Pour indiquer la source:
    typings install <nom de la source>~<nom du package> --save 
    

    Ou

    typings install <nom du package> --source <nom de la source> --save 
    

    Par défaut, la source de typings est npm.

    D’autres sources sont possibles, par exemple:

    • github pour récupérer des dépendances directement de GitHub (par exemple: Duo, JSPM).
    • bower pour la source Bower.
    • env pour des environments particulier (par exemple atom, electron). Il faut rajouter l’option --global.
    • dt pour la source DefinitelyTyped. Il faut rajouter l’option --global.
  • Pour installer une version spécifique:

    typings install <nom du package>@<version à installer> 
    

Dans le cas de notre exemple, il faut d’abord installer typings de façon globale en exécutant la commande:

~/external_typescript_modules/% npm install typings --global

On installe ensuite les fichiers de définition de façon globale et en utilisant la source dt:

~/external_typescript_modules/% typings install jquery --source dt --save --global 
~/external_typescript_modules/% typings install datatables.net --source dt --save --global 

Sans aucune mention supplémentaire, les types sont reconnus dans le code Typescript. Si on regarde les fichiers de types dans le répertoire typings, on remarque que le fichier typings/index.d.ts contient les lignes suivantes:

/// <reference path="globals/datatables.net/index.d.ts" /> 
/// <reference path="globals/jquery/index.d.ts" /> 

Ces lignes font des références vers les fichiers de définition pour respectivement:

  • jQuery dans typings/globals/jquery/index.d.ts
  • DataTables dans typings/globals/datatables.net/index.d.ts

De même que précédemment, avec l’ajout de ces fichiers de définition, la compilation réussit.

Le code final de cet exemple se trouve dans la branche 6_webpack_typings du repository GitHub msoft/external_typescript_modules.

Conclusion

L’import de bibliothèques Javascript externes dans du code Typescript est quasiment incontournable. La méthode la plus usuelle pour intégrer des bibliothèques courantes est d’importer des fichiers de définition avec npm. D’autres méthodes existent, toutefois elles sont réservées aux cas particuliers. Par exemple quand on souhaite utiliser une version non disponible avec npm ou quand simplement les fichiers de définition ne sont pas fournis.
Cet article a tenté d’illustrer le plus simplement, l’utilisation du mot-clé declare et l’import de fichiers de définition avec npm.

(1) – Add js extension to import/export: https://github.com/Microsoft/TypeScript/issues/18971
(2) – Provide a way to add the ‘.js’ file extension to the end of module specifiers: https://github.com/Microsoft/TypeScript/issues/16577

Références

Leave a Reply