Lecture de fond :
Classes (MDN)
TypeScript offre un support entier pour le mot-clé class
introduit avec ES2015.
Tout comme les autres fonctionnalités JavaScript, TypeScript ajoute les annotations de type et autres éléments de syntaxe pour vous permettre d’exprimer les relations entre les classes et les autres types.
Membres de classe
Voici une classe vide, la plus basique qui soit :
tsTry
classPoint {}
Cette classe n’est pas très utile, donc il va falloir ajouter quelques champs.
Champs
Une déclaration de champ crée une propriété écrivable et publique dans la classe :
tsTry
classPoint {x : number;y : number;}constpt = newPoint ();pt .x = 0;pt .y = 0;
Tout comme à d’autres endroits, l’annotation de type est optionnelle, mais sera any
par défaut.
Les champs peuvent aussi être initialisés automatiquement, dès que la classe est instanciée :
tsTry
classPoint {x = 0;y = 0;}constpt = newPoint ();// Prints 0, 0console .log (`${pt .x }, ${pt .y }`);
Tout comme avec const
, let
, et var
, l’initialiseur d’une propriété de classe va être utilisé pour inférer le type de la propriété:
tsTry
constpt = newPoint ();Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.pt .x = "0";
--strictPropertyInitialization
L’option strictPropertyInitialization
permet de s’assurer que les champs de la classe sont initialisés dans le constructeur.
tsTry
classBadGreeter {Property 'name' has no initializer and is not definitely assigned in the constructor.2564Property 'name' has no initializer and is not definitely assigned in the constructor.: string; name }
tsTry
classGoodGreeter {name : string;constructor() {this.name = "hello";}}
Remarquez que le champ doit être initialisé dans le constructeur lui-même. TypeScript n’analyse pas les méthodes que vous appelez dans le constructeur pour détecter les initialisations, parce qu’une classe dérivée pourrait surcharger ces méthodes et, par conséquent, ne pas initialiser ces champs.
Si vous voulez exprimer que le champ sera certainement initialisé, mais d’une façon différente qu’avec le constructeur (par exemple, une librairie externe qui remplit votre classe en partie), vous pouvez utiliser l’opérateur d’assertion garantie d’assignation, !
:
tsTry
classOKGreeter {// Not initialized, but no errorname !: string;}
readonly
Un champ peut être préfixé avec le modificateur readonly
.
Cela permet d’empêcher les assignations en dehors du constructeur.
tsTry
classGreeter {readonlyname : string = "world";constructor(otherName ?: string) {if (otherName !==undefined ) {this.name =otherName ;}}err () {this.Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.= "not ok"; name }}constg = newGreeter ();Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.g .= "also not ok"; name
Constructeurs
Lecture de fond :
Constructeur (MDN)
Les constructeurs de classes sont très similaires aux fonctions. Vous pouvez ajouter des paramètres avec des annotations de type, des valeurs par défaut, ainsi que des surcharges :
tsTry
classPoint {x : number;y : number;// Normal signature with defaultsconstructor(x = 0,y = 0) {this.x =x ;this.y =y ;}}
tsTry
classPoint {// Overloadsconstructor(x : number,y : string);constructor(s : string);constructor(xs : any,y ?: any) {// TBD}}
Il y a quelques différences entre une signature de constructeur et une signature de fonction classique :
- Un constructeur ne peut pas avoir de paramètre de type - ce dernier appartient à la déclaration externe de classe, qu’on apprendra plus tard.
- Un constructeur ne peut pas retourner autre chose que le type de l’instance de classe.
Appels de Super
Comme en JavaScript, si vous héritez d’une classe, vous devez appeler super();
dans votre constructeur avant tout usage de membres de this.
:
tsTry
classBase {k = 4;}classDerived extendsBase {constructor() {// Prints a wrong value in ES5; throws exception in ES6'super' must be called before accessing 'this' in the constructor of a derived class.17009'super' must be called before accessing 'this' in the constructor of a derived class.console .log (this .k );super();}}
Oublier d’appeler super
est une erreur courante en JavaScript, mais TypeScript vous en informera si nécessaire.
Méthodes
Lecture de fond :
Définir une méthode
Une propriété de classe qui est une fonction est appelée une méthode. Les méthodes peuvent se servir des mêmes annotations de type que les fonctions et les constructeurs :
tsTry
classPoint {x = 10;y = 10;scale (n : number): void {this.x *=n ;this.y *=n ;}}
En dehors des annotations normales de type, TypeScript n’ajoute rien d’autre aux méthodes.
Remarquez qu’à l’intérieur de la méthode, il est toujours obligatoire de préfixer d’autres méthodes et propriétés avec this.
.
Un nom de variable non initialisé dans une méthode va toujours se référer à la variable correspondante dans la portée qui l’englobe :
tsTry
letx : number = 0;classC {x : string = "hello";m () {// This is trying to modify 'x' from line 1, not the class propertyType 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.= "world"; x }}
Accesseurs / Mutateurs
Les classes peuvent aussi avoir des accesseurs:
tsTry
classC {_length = 0;getlength () {return this._length ;}setlength (value ) {this._length =value ;}}
Notez qu’une paire accesseur/mutateur sans logique supplémentaire est très rarement utile en JavaScript. Vous pouvez exposer une propriété publique si le mutateur et l’accesseur n’auront aucune logique associée.
TypeScript a des règles spéciales d’inférence pour les accesseurs :
- Si
get
existe mais passet
, la propriété est automatiquementreadonly
- Si le type de retour du mutateur n’est pas spécifié, il sera inféré de ce que retourne l’accesseur
- Le mutateur et l’accesseur doivent avoir la même Visibilité de Membres
Depuis TypeScript 4.3, il est possible d’avoir des types différents pour une paire d’accesseur et mutateur.
tsTry
classThing {_size = 0;getsize (): number {return this._size ;}setsize (value : string | number | boolean) {letnum =Number (value );// Don't allow NaN, Infinity, etcif (!Number .isFinite (num )) {this._size = 0;return;}this._size =num ;}}
Signatures d’Index
Les classes peuvent déclarer des signatures d’index; elles fonctionnent de la même manière que les Signatures d’Index pour les types objet:
tsTry
classMyClass {[s : string]: boolean | ((s : string) => boolean);check (s : string) {return this[s ] as boolean;}}
Il n’est pas aisé d’utiliser ces types de façon productive, parce que les signatures d’index doivent aussi définir les types de retour des méthodes indexées. Généralement, il vaut mieux stocker les données indexées autre part que sur l’instance de classe elle-même.
Héritage
Tout comme les autres langages avec des fonctionnalités orientées objet, les classes JavaScript peuvent hériter de classes parentes.
Clauses implements
Vous pouvez utiliser une clause implements
pour vérifier qu’une classe respecte une certaine interface
.
Dans le cas contraire, une erreur sera lancée :
tsTry
interfacePingable {ping (): void;}classSonar implementsPingable {ping () {console .log ("ping!");}}classClass 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.2420Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.implements Ball Pingable {pong () {console .log ("pong!");}}
Les classes peuvent implémenter plusieurs interfaces (par exemple, class C implements A, B {
).
Important
Il est important de comprendre que la clause implements
n’est qu’une vérification si la classe peut se substituer à l’interface implémentée.
La clause ne modifie pas du tout la classe, ni son type, ni ses méthodes. Une erreur commune est de supposer le contraire.
tsTry
interfaceCheckable {check (name : string): boolean;}classNameChecker implementsCheckable {Parameter 's' implicitly has an 'any' type.7006Parameter 's' implicitly has an 'any' type.check () { s // Pas d'erreur ici, le type de s étant "any"returns .toLowercse () === "ok";}}
Dans cet exemple, on pourrait s’attendre à ce que le type de s
puisse être influencé par le paramètre name: string
de check
.
Ce n’est pas le cas - la clause implements
ne change ni la façon de vérifier le corps de la classe, ni la façon d’inférer son type.
De façon similaire, implémenter une interface avec une propriété optionnelle ne crée pas cette propriété :
tsTry
interfaceA {x : number;y ?: number;}classC implementsA {x = 0;}constc = newC ();Property 'y' does not exist on type 'C'.2339Property 'y' does not exist on type 'C'.c .= 10; y
Clauses extends
Lecture de fond :
mot-clé extends(MDN)
Une classe peut extend
d’une classe-mère, et devient une classe dérivée.
Une classe dérivée reçoit toutes ses méthodes et propriétés de la classe-mère. Il est également possible de définir des membres additionnels.
tsTry
classAnimal {move () {console .log ("On bouge !");}}classDog extendsAnimal {woof (times : number) {for (leti = 0;i <times ;i ++) {console .log ("wouaf !");}}}constd = newDog ();// Base class methodd .move ();// Derived class methodd .woof (3);
Surchargement de méthodes
Lecture de fond:
mot-clé super (MDN)
Une classe dérivée peut aussi écraser une propriété ou une méthode préexistantes.
Les méthodes de la classe-mère sont accessibles avec le mot-clé super
.
Notez que, vu que les classes JavaScript ne sont que des objets, il n’y a pas de “champ super” qui donnerait une référence vers la classe-mère.
TypeScript impose que la classe dérivée soit un sous-type de la classe-mère.
Par exemple, voici une façon légale de surcharger une méthode :
tsTry
classBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {greet (name ?: string) {if (name ===undefined ) {super.greet ();} else {console .log (`Bonjour, ${name .toUpperCase ()}`);}}}constd = newDerived ();d .greet ();d .greet ("lecteur / lectrice");
Il est important que la classe dérivée suive le contrat imposé par sa classe-mère. Rappelez-vous qu’il est très commun (et toujours légal) d’instancier une classe dérivée tout en référençant une classe-mère :
tsTry
// Alias the derived instance through a base class referenceconstb :Base =d ;// No problemb .greet ();
Et si Derived
ne suivait pas le contrat de Base
?
tsTry
classBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {// Make this parameter requiredProperty 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.2416Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'. Target signature provides too few arguments. Expected 1 or more, but got 0.( greet name : string) {console .log (`Bonjour, ${name .toUpperCase ()}`);}}
Si on compile le code malgré l’erreur et qu’on le lance, il va simplement provoquer une erreur :
tsTry
constb :Base = newDerived ();// Crashes because "name" will be undefinedb .greet ();
Champs déclarés exclusivement avec un type
Si target
est plus grand ou est égal à ES2022
ou useDefineForClassFields
vaut true
, les champs de classes dérivées sont créés après la classe-mère, ce qui effacera toute valeur définie par cette classe-mère. Cela peut poser problème quand tout ce que vous souhaitez faire est de déclarer un type plus précis d’un champ hérité. Pour gérer ces cas d’usage, utilisez le mot-clé declare
pour indiquer à TypeScript que vous souhaitez uniquement typer le champ, sans lui assigner quoi que ce soit.
tsTry
interfaceAnimal {dateOfBirth : any;}interfaceDog extendsAnimal {breed : any;}classAnimalHouse {resident :Animal ;constructor(animal :Animal ) {this.resident =animal ;}}classDogHouse extendsAnimalHouse {// Le "declare" n'émettra pas de code JavaScript,// il s'assurera uniquement que le type de "resident" est correctdeclareresident :Dog ;constructor(dog :Dog ) {super(dog );}}
Ordre d’initialisation
L’ordre d’initialisation des classes JavaScript peut surprendre dans certains cas. Considérons ce code :
tsTry
classBase {name = "base";constructor() {console .log ("Mon nom est " + this.name );}}classDerived extendsBase {name = "derived";}// Affiche "base", et pas "derived"constd = newDerived ();
Qu’est-ce qui s’est passé ?
Tel que défini par JavaScript, voici l’ordre d’initialisation de classes :
- Les champs de la classe-mère sont initialisés
- Le constructeur de la classe-mère est lancé
- Les champs des classes dérivées sont initialisés
- Les constructeurs des classes dérivées sont lancés
Cela signifie que la classe-mère a d’abord appliqué sa propre valeur de name
, parce que le constructeur de la classe dérivée ne s’est pas encore lancé.
Héritage de classes intégrées
Note : Si vous ne comptez pas hériter de classes fournies par JavaScript, tel
Array
,Error
,Map
, etc. ou votre cible de compilation est une version supérieure ou égale àES6
/ES2015
, vous pouvez passer cette section.
Dans ES2015, les constructeurs qui retournent un objet remplacent implicitement toute référence de this
par toute classe appelant super(...)
.
Il est nécessaire que le code qui génère le constructeur capture toute valeur retournée par super(...)
et la remplace par this
.
De ce fait, il se peut que créer une sous-classe d’Error
ou d’Array
ne fonctionne plus comme prévu.
La raison est que les constructeurs d’Error
, Array
, etc. utilisent la propriété fournie par ES2015, new.target
, pour ajuster la chaîne de prototypes. Les versions qui précèdent ES6 ne fournissent aucun moyen de fournir de valeur pour new.target
.
Les autres compilateurs qui nivellent par le bas ont généralement des limites similaires.
Pour une classe dérivée, comme dans cet exemple :
tsTry
classMsgError extendsError {constructor(m : string) {super(m );}sayHello () {return "bonjour " + this.message ;}}
vous remarquerez peut-être que :
- les méthodes pourraient être
undefined
dans les objets retournés par ces classes dérivées, donc appelersayHello
va provoquer une erreur. instanceof
n’agira pas correctement entre les sous-classes et leurs classes-mères, donc(new MsgError()) instanceof MsgError
retournerafalse
.
Nous vous recommandons d’ajuster manuellement le prototype immédiatement après tout appel de super(...)
.
tsTry
classMsgError extendsError {constructor(m : string) {super(m );// Set the prototype explicitly.Object .setPrototypeOf (this,MsgError .prototype );}sayHello () {return "hello " + this.message ;}}
Cependant, toute sous-classe de MsgError
va devoir elle aussi réparer manuellement son propre prototype après super
.
Les moteurs qui ne supportent pas Object.setPrototypeOf
, fournissent la propriété __proto__
comme alternative.
Malheureusement, toutes ces solutions de contournement, ne fonctionneront pas sur Internet Explorer 10 et antérieur.
Vous pouvez copier manuellement les propriétés du prototype vers l’instance (par ex. MsgError.prototype
dans this
), mais la chaîne de prototypes elle-même ne pourra pas être réparée.
Visibilité de Membres
Vous pouvez utiliser TypeScript pour contrôler l’exposition de méthodes et propriétés de la classe vers le code qui lui est externe.
public
La visibilité par défaut de tout membre de classe est public
.
Il est possible d’accéder à un membre public
partout :
tsTry
classGreeter {publicgreet () {console .log ("salut !");}}constg = newGreeter ();g .greet ();
Parce que public
est déjà la visibilité par défaut, vous n’avez pas besoin de le préciser pour un membre de classe, mais vous pourriez toujours le faire pour des raisons de lisibilité / style de code.
protected
Les membres protected
ne sont visibles que dans la classe qui les a déclarés.
tsTry
classGreeter {publicgreet () {console .log ("Bonjour, " + this.getName ());}protectedgetName () {return "hi";}}classSpecialGreeter extendsGreeter {publichowdy () {// On peut accéder à this.getName iciconsole .log ("Yo, " + this.getName ());}}constg = newSpecialGreeter ();g .greet (); // OKProperty 'getName' is protected and only accessible within class 'Greeter' and its subclasses.2445Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.g .(); getName
Exposition des membres protected
Les classes dérivées doivent suivre les contrats de leurs classes de base, mais peuvent exposer un sous-type qui a plus de possibilités qu’une classe-mère.
Ainsi, il est possible de donner une visibilité public
à des membres protected
à l’origine :
tsTry
classBase {protectedm = 10;}classDerived extendsBase {// Pas de modificateur, le "public" par défaut s'appliquem = 15;}constd = newDerived ();console .log (d .m ); // OK
Remarquez que Derived
est quand même capable de lire et d’écrire m
, donc protéger m
n’aura servi à rien.
Si vous voulez rendre la propriété protected
dans la classe dérivée également, vous devrez répéter le mot-clé protected
.
Accès aux membres protected
entre classes mères et dérivées
Les langages OOP différents ne s’accordent pas si un membre qui est protected
est toujours accessible aux classes dérivées :
tsTry
classBase {protectedx : number = 1;}classDerived1 extendsBase {protectedx : number = 5;}classDerived2 extendsBase {f1 (other :Derived2 ) {other .x = 10;}f2 (other :Base ) {Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.2446Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.other .= 10; x }}
Java considère cette manipulation légale, au contraire du C++ et du C#.
TypeScript se range du côté du C# et C++ dans ce débat. Accéder à x
dans Derived2
doit être légal uniquement à partir de sous-classes de Derived2
, ce qui n’est pas le cas de Derived1
.
De plus, si l’accès à x
à travers une Derived1
est illégal pour des raisons évidentes, alors tenter d’y accéder à travers Base
ne doit rien y changer.
Voir aussi Why Can’t I Access A Protected Member From A Derived Class? qui explique le raisonnement derrière cette interdiction en C#.
private
private
ressemble à protected
, mais interdit tout accès à la propriété depuis autre chose que la classe elle-même (cela exclut donc les classes dérivées):
tsTry
classBase {privatex = 0;}constb = newBase ();// Can't access from outside the classProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (b .); x
tsTry
classDerived extendsBase {showX () {// Can't access in subclassesProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (this.); x }}
Une classe dérivée ne peut pas modifier la visibilité d’un membre private
, vu qu’elle ne le voit même pas :
tsTry
classBase {privatex = 0;}classClass 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.2415Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.extends Derived Base {x = 1;}
Accès à un membre private
entre différentes instances
Les langages OOP différents ne s’accordent pas si les instances d’une même classe peuvent accéder à leurs membres privés respectifs. Java, C#, C++, Swift, et PHP le permettent, Ruby l’interdit.
TypeScript le permet :
tsTry
classA {privatex = 10;publicsameAs (other :A ) {// No errorreturnother .x === this.x ;}}
Considérations
Comme d’autres aspects de TypeScript, private
et protected
sont uniquement imposés pendant la compilation.
Cela signifie que des expressions JavaScript in
ou une simple lecture de propriétés peuvent accéder à un membre private
ou protected
:
tsTry
classMySafe {privatesecretKey = 12345;}
js
// Dans un fichier JavaScript, va afficher 12345const s = new MySafe();console.log(s.secretKey);
private
permet également d’accéder à la propriété avec la notation à crochets. Cela permet de faciliter l’accès aux propriétés private
pour, par exemple, les tests unitaires. Le défaut dans cette approche est que ces propriétés ne sont donc pas complètement private
.
tsTry
classMySafe {privatesecretKey = 12345;}consts = newMySafe ();// Interdit durant la vérificationProperty 'secretKey' is private and only accessible within class 'MySafe'.2341Property 'secretKey' is private and only accessible within class 'MySafe'.console .log (s .); secretKey // OKconsole .log (s ["secretKey"]);
Les variables de classes privées (#
) resteront privées après compilation et représentent une approche plus stricte aux champs privés, interdisant les contournements disponibles avec le mot-clé private
.
tsTry
classDog {#barkAmount = 0;personality = "happy";constructor() {}}
tsTry
"use strict";class Dog {#barkAmount = 0;personality = "happy";constructor() { }}
En compilant vers ES2021 ou inférieur, TypeScript va utiliser des WeakMaps
à la place de #
.
tsTry
"use strict";var _Dog_barkAmount;class Dog {constructor() {_Dog_barkAmount.set(this, 0);this.personality = "happy";}}_Dog_barkAmount = new WeakMap();
Si vous avez besoin de protéger vos valeurs de classes contre les acteurs malicieux, vous devez vous servir de mécanismes offrant de la sécurité stricte durant l’exécution, tel que les closures, WeakMaps, ou les champs privés. Remarquez que ces mesures additionnelles peuvent affecter la performance.
Membres statiques
Lecture de fond :
Membres statiques (MDN)
Les Classes peuvent avoir des membres static
.
Ces membres ne sont pas associés à une instance particulière d’une classe, et peuvent être lus depuis le constructeur de la classe elle-même :
tsTry
classMyClass {staticx = 0;staticprintX () {console .log (MyClass .x );}}console .log (MyClass .x );MyClass .printX ();
Les membres static
peuvent avoir les mêmes modificateurs public
, protected
, et private
:
tsTry
classMyClass {private staticx = 0;}Property 'x' is private and only accessible within class 'MyClass'.2341Property 'x' is private and only accessible within class 'MyClass'.console .log (MyClass .); x
Les membres static
peuvent être hérités par les classes dérivées :
tsTry
classBase {staticgetGreeting () {return "Hello world";}}classDerived extendsBase {myGreeting =Derived .getGreeting ();}
Noms spéciaux de propriétés statiques
Généralement, il n’est pas sûr / possible d’écrire sur des propriétés du prototype de Function
.
Les classes sont elles-mêmes des fonctions qui peuvent être invoquées avec new
. Donc certaines propriétés static
ne peuvent pas être utilisées.
Les propriétés name
, length
, et call
ne peuvent pas être définies en tant que membres static
:
tsTry
classS {staticStatic property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.2699Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.= "S!"; name }
Pourquoi pas des classes statiques ?
TypeScript (et JavaScript) n’ont pas de classes statiques, de la même façon que, par exemple, C#.
Ces structures n’existent que parce que ces langages obligent toutes les données et fonctions à être à l’intérieur de classes. Elles n’ont aucun intérêt à être dans TypeScript ou JavaScript, ces deux langages n’ayant pas cette restriction. Une classe qui n’a qu’une seule instance est parfois représentée simplement par un objet normal.
Une classe statique n’est pas nécessaire car elle peut très bien se substituer à un objet ou une fonction :
tsTry
// Classe statique non nécessaireclassMyStaticClass {staticdoSomething () {}}// 1ère alternative privilégiéefunctiondoSomething () {}// 2ème alternative privilégiéeconstMyHelperObject = {dosomething () {},};
Blocs static
dans une classe
Les blocs statiques vous permettent d’écrire des déclarations avec leur propre portée. Cette portée peut lire les champs privés dans la classe qui les contient. Cela signifie que l’on peut écrire ce qu’on veut en termes de code, sans fuite de variables vers l’extérieur, et avec accès complet aux propriétés et méthodes de la classe.
tsTry
classFoo {static #count = 0;getcount () {returnFoo .#count;}static {try {constlastInstances =loadLastInstances ();Foo .#count +=lastInstances .length ;}catch {}}}
Classes Génériques
Les classes, au même titre des interfaces, peuvent être génériques.
Quand une classe est instanciée avec new
, ses paramètres de type peuvent être inférés de la même façon qu’avec une fonction :
tsTry
classBox <Type > {contents :Type ;constructor(value :Type ) {this.contents =value ;}}constb = newBox ("bonjour !");
Les classes peuvent imposer des restrictions de type et des types par défaut tout comme les interfaces.
Paramètres de type dans les propriétés statiques
Il peut être difficile de comprendre pourquoi ce code est illégal :
tsTry
classBox <Type > {staticStatic members cannot reference class type parameters.2302Static members cannot reference class type parameters.defaultValue :; Type }
Rappelez-vous qu’à l’exécution, les données de types sont complètement effacées !
Il n’existe qu’une seule valeur possible (et donc un seul type possible) à la propriété Box.defaultValue
.
Cela signifie que définir Box<string>.defaultValue
(si c’était possible) changerait également Box<number>.defaultValue
- pas idéal.
Donc les membres static
d’une classe générique ne peuvent pas faire de référence aux types génériques de la classe.
this
à l’exécution dans les classes
Lecture de fond :
L'opérateur this (MDN)
TypeScript ne change pas le comportement de JavaScript à l’exécution, et JavaScript est célèbre pour ses comportements très particuliers à l’exécution.
Cela inclut l’opérateur this
:
tsTry
classMyClass {name = "MyClass";getName () {return this.name ;}}constc = newMyClass ();constobj = {name : "obj",getName :c .getName ,};// Affiche "obj", pas "MyClass"console .log (obj .getName ());
Pour résumer, par défaut, la valeur de this
à l’intérieur d’une fonction dépend de comment la fonction a été appelée.
Dans cet exemple, parce que cette fonction a été appelée avec une référence à obj
, la valeur de this
était obj
au lieu d’être l’instance de classe.
C’est rarement le comportement que vous désirez ! TypeScript fournit plusieurs façons de remédier à ce problème.
Fonctions fléchées
Lecture de fond :
Fonctions fléchées (MDN)
Si vous avez une fonction qui va être appelée et va être amenée à perdre le contexte de this
, cela peut être judicieux d’utiliser une propriété de fonction fléchée au lieu d’une définition de méthode plus classique :
tsTry
classMyClass {name = "MyClass";getName = () => {return this.name ;};}constc = newMyClass ();constg =c .getName ;// Affiche "MyClass" au lieu de crashconsole .log (g ());
Cette façon de faire impose quelques compromis :
- La valeur de
this
est toujours correcte à l’exécution, même avec du code qui n’est pas vérifié par TypeScript. - Les fonctions fléchées consomment plus de mémoire, parce que chaque fonction aura sa propre copie de fonctions définies de cette façon.
- Vous ne pouvez pas vous servir de
super.getName
dans une classe dérivée, parce qu’il n’existe aucune classe de base dans la chaîne de prototypes d’où il est possible de récupérer la méthode.
Paramètre this
Dans une définition de méthode ou de fonction, il est possible d’ajouter un paramètre this
qui a un sens spécial en TypeScript.
Ce paramètre est effacé à la compilation :
tsTry
// Code TypeScript avec le paramètre 'this'functionfn (this :SomeType ,x : number) {/* ... */}
js
// Sortie JavaScriptfunction fn(x) {/* ... */}
TypeScript vérifie qu’un appel de fonction avec un paramètre this
est fait avec un contexte correct.
Au lieu d’utiliser une fonction fléchée, il est possible d’utiliser un paramètre this
dans les définitions de méthodes pour vérifier qu’elles ont été appelées correctement :
tsTry
classMyClass {name = "MyClass";getName (this :MyClass ) {return this.name ;}}constc = newMyClass ();// OKc .getName ();// Erreur, crashconstg =c .getName ;The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.2684The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.console .log (g ());
Cette façon opte pour les compromis opposés à l’approche de la fonction fléchée :
- Les entités JavaScript qui appellent ces méthodes pourraient utiliser le mauvais contexte de
this
sans s’en rendre compte. - Seule une fonction par définition de classe sera allouée, au lieu d’une par instance de classe.
- Les définitions de méthode de base peuvent toujours être appelées via
super
.
Types this
Dans une classe, un type spécial this
réfère dynamiquement au type de la classe courante.
Voici un exemple où cela pourrait être utile :
tsTry
classBox {contents : string = "";set (value : string) {this.contents =value ;return this;}}
TypeScript a inféré le type de retour du set
comme étant this
, au lieu de Box
.
Créons une classe dérivée de Box
:
tsTry
classClearableBox extendsBox {clear () {this.contents = "";}}consta = newClearableBox ();constb =a .set ("bonjour");
Vous pouvez aussi utiliser this
dans une annotation de type de paramètre :
tsTry
classBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}
La différence avec l’écriture other: Box
est que si vous avez une classe dérivée, sa méthode sameAs
ne va accepter qu’une autre instance de cette classe dérivée :
tsTry
classBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}classDerivedBox extendsBox {otherContent : string = "?";}constbase = newBox ();constderived = newDerivedBox ();Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.2345Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.derived .sameAs (); base
Gardes de types sur this
Vous pouvez utiliser this is Type
dans la position de type de retour dans les méthodes à l’intérieur de classes ou interfaces.
Avec le rétrécissement de types (par ex. les déclarations if
), le type de l’objet peut être rétréci vers le Type
spécifié.
tsTry
classFileSystemObject {isFile (): this isFileRep {return this instanceofFileRep ;}isDirectory (): this isDirectory {return this instanceofDirectory ;}isNetworked (): this isNetworked & this {return this.networked ;}constructor(publicpath : string, privatenetworked : boolean) {}}classFileRep extendsFileSystemObject {constructor(path : string, publiccontent : string) {super(path , false);}}classDirectory extendsFileSystemObject {children :FileSystemObject [];}interfaceNetworked {host : string;}constfso :FileSystemObject = newFileRep ("foo/bar.txt", "foo");if (fso .isFile ()) {fso .content ;} else if (fso .isDirectory ()) {fso .children ;} else if (fso .isNetworked ()) {fso .host ;}
Un cas d’usage commun pour ces gardes de types de this
est de permettre une validation passive d’un champ particulier. Par exemple, ce cas retire undefined
des possibilités de valeur de this.value
, si hasValue
retourne true
:
tsTry
classBox <T > {value ?:T ;hasValue (): this is {value :T } {return this.value !==undefined ;}}constbox = newBox ();box .value = "Gameboy";box .value ;if (box .hasValue ()) {box .value ;}
Propriétés-Paramètres
TypeScript offre une syntaxe spéciale qui crée une propriété avec les mêmes modificateur, nom et valeur que le paramètre fourni.
Ce sont des propriétés-paramètres qui sont créées en préfixant un argument de constructeur avec l’un des modificateurs public
, private
, protected
, ou readonly
.
Le champ résultant hérite de ces modificateurs :
tsTry
classParams {constructor(public readonlyx : number,protectedy : number,privatez : number) {// Pas de corps nécessaire}}consta = newParams (1, 2, 3);console .log (a .x );Property 'z' is private and only accessible within class 'Params'.2341Property 'z' is private and only accessible within class 'Params'.console .log (a .); z
Expressions de Classes
Lecture de fond :
Expression de Classe (MDN)
Les expressions de classe sont similaires aux déclarations de classes, à une différence près : les expressions de classes n’ont pas besoin de noms, même si on peut y référer avec l’identifiant qu’on lui associera :
tsTry
constsomeClass = class<Type > {content :Type ;constructor(value :Type ) {this.content =value ;}};constm = newsomeClass ("Bonjour tout le monde !");
Classes et membres abstract
Les champs, méthodes et classes peuvent être abstraits.
Une méthode abstraite ou un champ abstrait n’a pas d’implémentation. Ces membres ne peuvent exister que dans une classe abstraite, qui ne peut pas être instanciée directement.
Le rôle d’une classe abstraite est de servir de classe-mère pour toutes les classes-filles qui implémentent les membres abstraits. Une classe concrète est une classe qui n’a pas de membre abstrait.
Prenons un exemple :
tsTry
abstract classBase {abstractgetName (): string;printName () {console .log ("Bonjour, " + this.getName ());}}constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.b = newBase ();
Nous ne pouvons pas instancier Base
avec new
parce que la classe est abstraite.
À la place, nous devons créer une classe dérivée et instancier cette classe :
tsTry
classDerived extendsBase {getName () {return "monde";}}constd = newDerived ();d .printName ();
Remarquez que si vous oubliez d’implémenter un membre abstrait, vous aurez une erreur :
tsTry
classNon-abstract class 'Derived' does not implement inherited abstract member getName from class 'Base'.2515Non-abstract class 'Derived' does not implement inherited abstract member getName from class 'Base'.extends Derived Base {// on a oublié de faire quoi que ce soit}
Signatures abstraites
Parfois, vous voudrez accepter un constructeur qui produit l’instance d’une classe dérivée d’une classe abstraite.
Prenez ce code par exemple :
tsTry
functiongreet (ctor : typeofBase ) {constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.instance = newctor ();instance .printName ();}
TypeScript devine correctement que vous essayez d’instancier une classe abstraite.
D’après la signature de greet
, ce code est légal, mais il construirait une classe abstraite :
tsTry
// Non !greet (Base );
À la place, vous voudrez accepter une fonction qui accepte une signature de constructeur :
tsTry
functiongreet (ctor : new () =>Base ) {constinstance = newctor ();instance .printName ();}greet (Derived );Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.2345Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.greet (); Base
Maintenant, TypeScript vous dira que vous pouvez utiliser Derived
car c’est une classe concrète, mais pas Base
.
Relation entre les classes
La plupart des cas avec TypeScript, les classes sont comparées avec leurs structures, comme avec les autres types.
Par exemple, ces deux classes sont utilisables et interchangeables, de par leurs structures identiques :
tsTry
classPoint1 {x = 0;y = 0;}classPoint2 {x = 0;y = 0;}// OKconstp :Point1 = newPoint2 ();
Similairement, une classe peut être un sous-type d’une autre même sans relation explicite d’héritage :
tsTry
classPerson {name : string;age : number;}classEmployee {name : string;age : number;salary : number;}// OKconstp :Person = newEmployee ();
Cela paraît évident, mais il y a certains cas plus bizarres que d’autres.
Les classes vides n’ont pas de membres. Dans un système de types structurel, un type sans membres est un super-type de tous les autres types. Donc si vous écrivez une classe vide (ce que vous ne devez pas faire), vous pouvez l’utiliser partout :
tsTry
classEmpty {}functionfn (x :Empty ) {// je ne peux rien faire avec 'x'}// Tout est OK !fn (window );fn ({});fn (fn );