
ainsi que les champs privés ECMAScript
Imports et exports d'un type en particulier
TypeScript réutilise la syntaxe d'importation de JavaScript afin de nous permettre de référencer les types. Par exemple, dans l'exemple suivant, nous pouvons importer doThing qui est une valeur JavaScript avec Options qui est purement un type TypeScript.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // ./foo.ts interface Options { // ... } export function doThing(options: Options) { // ... } // ./bar.ts import { doThing, Options } from "./foo.js"; function doThingBetter(options: Options) { // do something twice as good doThing(options); doThing(options); } |
C'est pratique parce que la plupart du temps, nous n'avons pas à nous soucier de ce qui est importé, mais uniquement de nous assurer que nous importons bien quelque chose. Malheureusement, cela n'a fonctionné qu'en raison d'une fonctionnalité appelée élision d'importation. Lorsque TypeScript génère des fichiers JavaScript, il voit que Options n'est utilisé que comme type et supprime automatiquement son importation. La sortie résultante ressemble un peu à ceci:
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | // ./foo.js export function doThing(options: Options) { // ... } // ./bar.js import { doThing } from "./foo.js"; function doThingBetter(options: Options) { // do something twice as good doThing(options); doThing(options); } |
Encore une fois, ce comportement est généralement bien, mais il provoque d'autres problèmes. Tout d’abord, il existe des endroits où il est ambigu d’exporter une valeur ou un type. Par exemple, dans l'exemple suivant, MyThing est-il une valeur ou un type ?
Code TypeScript : | Sélectionner tout |
1 2 3 | import { MyThing } from "./some-module.js"; export { MyThing }; |
En nous limitant à ce fichier, il n'y a aucun moyen de le savoir. L'API transpileModule de Babel et de TypeScript émettra du code qui ne fonctionnera pas correctement si MyThing n'est qu'un type, et l'indicateur isolatedModules de TypeScript nous avertira que ce sera un problème. Le vrai problème ici est qu'il n'y a aucun moyen de dire « non non, en fait je voulais faire appel au type, ceci devrait être effacé », donc l'importation d'élision n'est pas suffisante.
L’autre problème était que l’élision d’importation de TypeScript supprimait les instructions d’importation qui ne contenaient que les importations utilisées comme types. Cela a provoqué un comportement sensiblement différent pour les modules qui ont des effets secondaires, et les utilisateurs devraient donc insérer une deuxième instruction d'importation uniquement pour contrer les effets secondaires.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 | // This statement will get erased because of import elision. import { SomeTypeFoo, SomeOtherTypeBar } from "./module-with-side-effects"; // This statement always sticks around. import "./module-with-side-effects"; |
Un cas concret où l'équipe TypeScript a vu cela arriver était dans des frameworks comme Angular.js (1.x) où les services devaient être enregistrés globalement (ce qui est un effet secondaire), mais où ces services n'étaient importés que pour les types.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | // ./service.ts export class Service { // ... } register("globalServiceId", Service); // ./consumer.ts import { Service } from "./service.js"; inject("globalServiceId", function (service: Service) { // do stuff with Service }); |
Par conséquent, ./service.js ne sera jamais exécuté et les choses vont planter lors de l'exécution.
Pour éviter ce type de problèmes, l'équipe a réalisé qu'elle devait donner aux utilisateurs un contrôle plus fin sur la façon dont les choses étaient importées/élidées.
En tant que solution dans TypeScript 3.8, l'équipe a ajouté une nouvelle syntaxe pour les importations et exportations de type uniquement.
Code TypeScript : | Sélectionner tout |
1 2 3 | import type { SomeThing } from "./some-module.js"; export type { SomeThing }; |
import type importe uniquement les déclarations à utiliser pour les annotations et les déclarations de type. Il est toujours entièrement effacé, il n'y a donc pas de reste à l'exécution. De même, export type fournit uniquement une exportation qui peut être utilisée pour les contextes de type et est également effacé de la sortie de TypeScript.
Il est important de noter que les classes ont une valeur au moment de l'exécution et un type au moment du design, et l'utilisation est très sensible au contexte. Lorsque vous utilisez import type pour importer une classe, vous ne pouvez pas faire des choses comme faire une extension à partir de celle-ci.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | import type { Component } from "react"; interface ButtonProps { // ... } class Button extends Component<ButtonProps> { // ~~~~~~~~~ // error! 'Component' only refers to a type, but is being used as a value here. // ... } |
Si vous avez déjà utilisé Flow, la syntaxe est assez similaire. Une différence est que l'équipe TypeScript a ajouté quelques restrictions pour éviter le code qui pourrait apparaître ambigu.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 | // Is only 'Foo' a type? Or every declaration in the import? // We just give an error because it's not clear. import type Foo, { Bar, Baz } from "some-module"; // ~~~~~~~~~~~~~~~~~~~~~~ // error! A type-only import can specify a default import or named bindings, but not both. |
En conjonction avec import type, l'équipe TypeScript a également ajouté un nouvel indicateur de compilateur pour contrôler ce qui se passe avec les importations qui ne seront pas utilisées au moment de l'exécution: importsNotUsedAsValues. Ce drapeau prend 3 valeurs différentes:
remove: c'est le comportement actuel de l'abandon de ces importations. Ce sera toujours la valeur par défaut.
preserve : cette valeur préserve toutes les importations dont les valeurs ne sont jamais utilisées. Cela peut entraîner la préservation des importations/effets secondaires.
error : cette valeur préserve toutes les importations (la même que l'option preserve), mais va provoquer une erreur lorsqu'une importation de valeur n'est utilisée que comme type. Cela peut être utile si vous voulez vous assurer qu'aucune valeur n'est importée accidentellement, tout en rendant les importations d'effets secondaires explicites.
Champs privés ECMAScript
TypeScript 3.8 prend en charge les champs privés d'ECMAScript, qui font partie de la proposition de champs de classe de stade 3.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Person { #name: string constructor(name: string) { this.#name = name; } greet() { console.log(`Hello, my name is ${this.#name}!`); } } let jeremy = new Person("Jeremy Bearimy"); jeremy.#name // ~~~~~ // Property '#name' is not accessible outside class 'Person' // because it has a private identifier. |
Contrairement aux propriétés régulières (même celles déclarées avec le modificateur private), les champs privés ont quelques règles à garder à l'esprit. Certaines d'entre elles sont:
- Les champs privés commencent par un caractère #. Parfois, nous les appelons noms privés.
- Chaque nom de champ privé a une portée unique dans sa classe conteneur.
- Les modificateurs d'accessibilité TypeScript comme public ou private ne peuvent pas être utilisés sur des champs privés.
- Les champs privés ne peuvent pas être accédés ou même détectés en dehors de la classe contenant.
Les déclarations de propriétés régulières sont sujettes à être écrasées dans les sous-classes :
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class C { foo = 10; cHelper() { return this.foo; } } class D extends C { foo = 20; dHelper() { return this.foo; } } let instance = new D(); // 'this.foo' refers to the same property on each instance. console.log(instance.cHelper()); // prints '20' console.log(instance.dHelper()); // prints '20' |
Avec les champs privés, vous n'aurez jamais à vous en préoccuper, car chaque nom de champ est unique à la classe contenante.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class C { #foo = 10; cHelper() { return this.#foo; } } class D extends C { #foo = 20; dHelper() { return this.#foo; } } let instance = new D(); // 'this.#foo' refers to a different field within each class. console.log(instance.cHelper()); // prints '10' console.log(instance.dHelper()); // prints '20' |
Une autre chose à noter est que l'accès à un champ privé sur n'importe quel autre type entraînera une TypeError!
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Square { #sideLength: number; constructor(sideLength: number) { this.#sideLength = sideLength; } equals(other: any) { return this.#sideLength === other.#sideLength; } } const a = new Square(100); const b = { sideLength: 100 }; // Boom! // TypeError: attempted to get private field on non-instance // This fails because 'b' is not an instance of 'Square'. console.log(a.equals(b)); |
Enfin, pour tout utilisateur de fichier .js ordinaire, les champs privés doivent toujours être déclarés avant d'être attribués.
[CODE=TypeScript]class C {
// No declaration for '#foo'
//

constructor(foo: number) {
// SyntaxError!
// '#foo' needs to be declared before writing to it.[/code=typescript]...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.