Une interface est une forme d’un objet. Un objet JavaScript standard est une carte de paires key:value. Les clés des objets JavaScript sont dans presque tous les cas des chaînes de caractères et leurs valeurs sont toutes les valeurs JavaScript prises en charge (primitives ou abstraites).

Une interface indique au compilateur TypeScript les noms de propriétés qu’un objet peut avoir et leurs types de valeurs correspondants. Par conséquent, l’interface est un type et est un type abstrait puisqu’elle est composée de types primitifs.

Lorsque nous définissons un objet avec des propriétés (clés) et des valeurs, TypeScript crée une interface implicite en regardant les noms de propriétés et le type de données de leurs valeurs dans l’objet. Cela se produit en raison de l’inférence de type.

(object-shape.ts)

Dans l’exemple ci-dessus, nous avons créé un objet student avec des champs firstName, lastName, age et getSalary et nous lui avons attribué certaines valeurs initiales. En utilisant ces informations, TypeScript crée un type d’interface implicite pour student.

{
firstName: string;
lastName: string;
age: number;
getSalary: (base: number) => number;
}

Une interface est comme un objet mais elle ne contient que les informations sur les propriétés de l’objet et leurs types. Nous pouvons également créer un type d’interface et lui donner un nom afin de l’utiliser pour annoter les valeurs des objets mais ici, cette interface n’a pas de nom puisqu’elle a été créée implicitement. Vous pouvez comparer cela avec le type de fonction dans la leçon précédente qui a été créé implicitement au début et ensuite nous avons créé un type de fonction explicitement en utilisant l’alias de type.

Essayons de jouer avec les propriétés de l’objet après qu’il ait été défini.

(shape-override.ts)

Comme vous pouvez le voir dans l’exemple ci-dessus, TypeScript se souvient de la forme d’un objet puisque le type de ross est l’interface implicite. Si nous essayons de remplacer la valeur d’une propriété par une valeur de type différent de ce qui est spécifié dans l’interface ou si nous essayons d’ajouter une nouvelle propriété qui n’est pas spécifiée dans l’interface, le compilateur TypeScript ne compilera pas le programme.

Si vous voulez qu’un objet ait fondamentalement n’importe quelle propriété, alors vous pouvez marquer explicitement une valeur any et le compilateur TypeScript ne déduira pas le type à partir de la valeur d’objet assignée. Il y a d’autres meilleures façons d’obtenir exactement cela et nous allons les passer en revue dans cet article.

(shape-override-any.ts)

Déclaration d’interface

Bien que l’interface implicite que nous avons vue jusqu’à présent soit techniquement un type, mais elle n’a pas été définie explicitement. Comme discuté, une interface n’est rien d’autre que la forme qu’un objet peut prendre. Si vous avez une fonction qui accepte un argument qui devrait être un objet mais d’une forme particulière, alors nous devons annoter cet argument (paramètre) avec un type d’interface.

(argument-avec-forme.ts)

Dans l’exemple ci-dessus, nous avons défini une fonction getPersonInfo qui accepte un argument objet qui a firstName, lastName, age et getSalary champs de types de données spécifiés. Remarquez que nous avons utilisé un objet qui contient des noms de propriétés et leurs types correspondants comme type en utilisant l’annotation :<type>. C’est un exemple d’interface anonyme puisque l’interface n’a pas de nom, elle a été utilisée en ligne.

Tout cela semble un peu compliqué à gérer. Si l’objet ross devient plus compliqué et qu’il doit être utilisé à plusieurs endroits, TypeScript semble juste une chose que vous avez aimé au départ, mais maintenant juste une chose difficile à gérer. Pour résoudre ce problème, nous définissons un type d’interface en utilisant le mot-clé interface.

(argument-with-interface.ts)

Dans l’exemple ci-dessus, nous avons défini une interface Person qui décrit la forme d’un objet, mais cette fois, nous avons un nom que nous pouvons utiliser pour faire référence à ce type. Nous avons utilisé ce type pour annoter la variable ross ainsi que l’argument person de la fonction getPersonIfo. Cela informera TypeScript de valider ces entités par rapport à la forme de Person.

Pourquoi utiliser une interface?

Le type interface peut être important pour faire respecter une forme particulière. Typiquement, en JavaScript, nous mettons une foi aveugle au moment de l’exécution qu’un objet contiendra toujours une propriété particulière et que cette propriété aura toujours une valeur d’un type particulier comme {age: 21, ...} à titre d’exemple.

Lorsque nous commençons réellement à effectuer des opérations sur cette propriété sans vérifier d’abord si cette propriété existe sur l’objet ou si sa valeur est ce que nous attendions, les choses peuvent mal tourner et cela peut laisser votre application inutilisable par la suite. Par exemple, {age: '21', ...}, ici age la valeur est un string.

Les interfaces fournissent un mécanisme sûr pour traiter de tels scénarios au moment de la compilation. Si vous utilisez accidentellement une propriété sur un objet qui n’existe pas ou si vous utilisez la valeur d’une propriété dans l’opération illégale, le compilateur TypeScript ne compilera pas votre programme. Voyons un exemple.

(interface-safety.ts)

Dans l’exemple ci-dessus, nous essayons d’utiliser la propriété name de l’argument _student à l’intérieur de la fonction printStudent. Comme l’argument _student est un type d’interface Student, le compilateur TypeScript jette une erreur pendant la compilation puisque cette propriété n’existe pas dans l’interface Student.

De même, 100 — _student.firstName n’est pas une opération valide puisque la propriété firstName est un type de string et la dernière fois que j’ai vérifié, vous ne pouvez pas soustraire un string d’un number est JavaScript (résultats dans NaN).

Dans l’exemple ci-dessus, nous avons utilisé la manière traditionnelle d’écrire le type de fonction pour le champ getSalary. Cependant, vous pouvez également utiliser la syntaxe de fonction sans le corps pour la même chose, ce qui est généralement utilisé dans les interfaces.

interface Student {
firstName: string;
lastName: string;
age: number;
getSalary(base: number): number;
};

Propriétés optionnelles

Parfois, vous avez besoin qu’un objet ait une propriété qui détient des données d’un type particulier, mais il n’est pas obligatoire d’avoir cette propriété sur l’objet. Ceci est similaire aux paramètres optionnels des fonctions que nous avons appris dans la leçon précédente.

Ces propriétés sont appelées propriétés optionnelles. Une interface peut contenir des propriétés optionnelles et nous utilisons l’annotation ?:Type pour les représenter, tout comme les paramètres de fonction optionnels.

(optional-properties.ts)

Dans l’exemple ci-dessus, l’interface Student possède la propriété age qui est optionnelle. Cependant, si la propriété age est fournie, elle doit avoir une valeur du type number.

Dans le cas de l’objet ross qui est un type de l’interface Student, nous n’avons pas fourni la valeur de la propriété age qui est légale, cependant, dans le cas de monica, nous avons fourni la propriété age mais sa valeur est string qui n’est pas légale. Par conséquent, le compilateur TypeScript jette une erreur.

L’erreur peut sembler bizarre mais elle est en fait logique. Si la propriété age n’existe pas sur un objet, le object.age renverra undefined qui est un type de undefined. Si elle existe, alors la valeur doit être du type number.

Hence la valeur de la propriété age peut être soit du type undefined, soit du type number qui, dans TypeScript, est représenté en utilisant la syntaxe d’union number | undefined.

💡 Nous apprendrons les unions de type dans une leçon sur le système de type.

Cependant, les propriétés optionnelles posent de sérieux problèmes pendant l’exécution du programme. Imaginons que nous utilisions la propriété age dans une opération arithmétique mais que sa valeur soit undefined. C’est une sorte de problème sérieux.

Mais la bonne chose est que le compilateur TypeScript ne permet pas d’effectuer des opérations illégales sur une propriété optionnelle puisque sa valeur peut être undefined.

(optional-properties-safety.ts)

Dans l’exemple ci-dessus, nous effectuons une opération arithmétique sur la propriété age qui est illégale car la valeur de cette propriété peut être number ou undefined dans le runtime. Effectuer des opérations arithmétiques sur undefined donne NaN (pas un nombre).

💡 Cependant, pour le programme ci-dessus, nous devions définir le drapeau --strictNullChecks à false qui est un drapeau de compilateur TypeScript. Si nous fournissons cette option, le programme ci-dessus se compile très bien.

Pour éviter cette erreur ou cet avertissement, nous devons dire explicitement au compilateur TypeScript que cette propriété est un type de number et non le number ou undefined. Pour cela, nous utilisons l’assertion de type (AKA conversion de type ou typecasting).

(optional-properties-safety-override.ts)

Dans le programme ci-dessus, nous avons utilisé (_student.age as number) qui convertit le type de _student.age de number | undefined en number. C’est une façon de dire au compilateur TypeScript, « Hé, ceci est un nombre ». Mais une meilleure façon de gérer cela serait de vérifier également si _student.age est undefined au moment de l’exécution, puis d’effectuer l’opération arithmétique.

💡 Nous apprendrons les assertions de type dans une leçon sur le système de type.

Type de fonction utilisant une interface

Non seulement la forme d’un objet ordinaire, mais une interface peut également décrire la signature d’une fonction. Dans la leçon précédente, nous avons utilisé l’alias de type pour décrire un type de fonction, mais les interfaces peuvent également le faire.

interface InterfaceName {
(param: Type): Type;
}

La syntaxe pour déclarer une interface comme type de fonction est similaire à la signature de la fonction elle-même. Comme vous pouvez le voir dans l’exemple ci-dessus, le corps de l’interface contient la signature exacte d’une fonction anonyme, sans le corps bien sûr. Ici, les noms des paramètres n’ont pas d’importance.

(function-signature.ts)

Dans l’exemple ci-dessus, nous avons défini IsSumOdd l’interface qui définit un type de fonction qui accepte deux arguments de type number et renvoie une valeur boolean. Maintenant, vous pouvez utiliser ce type pour décrire une fonction car le type d’interface IsSumOdd est équivalent au type de fonction (x: number, y: number) => boolean.

Une interface avec une signature de méthode anonyme décrit une fonction. Mais une fonction dans le royaume JavaScript est également un objet, ce qui signifie que vous pouvez ajouter des propriétés à une valeur de fonction tout comme un objet. Il est donc parfaitement légal que vous puissiez définir n’importe quelles propriétés sur une interface de type fonction.

(function-signature-with-methods.ts)

Dans l’exemple ci-dessus, nous avons ajouté des propriétés type et calculate sur l’interface IsSumOdd qui décrit une fonction. En utilisant la méthode Object.assign, nous fusionnons les propriétés type et calculate avec une valeur de fonction.

Les interfaces du type fonction peuvent être utiles pour décrire les fonctions constructeurs. Une fonction constructeur est similaire à une classe dont le travail consiste à créer des objets (instances). Jusqu’à ES5, nous n’avions que des fonctions de constructeur pour imiter un class en JavaScript. Par conséquent, TypeScript compile les classes en fonctions constructeurs si vous ciblez ES5 ou moins.

💡 Si vous voulez en savoir plus sur la fonction constructeur, suivez cet article.

Si nous mettons le mot-clé new avant la signature de la fonction anonyme dans l’interface, cela rend la fonction constructible. Cela signifie que la fonction ne peut être invoquée qu’en utilisant le mot-clé new pour générer des objets et non en utilisant un appel de fonction ordinaire. Un exemple de fonction constructrice ressemble à ce qui suit.

function Animal( _name ) {
this.name = _name;
}var dog = new Animal( 'Tommy' );
console.log( dog.name ); // Tommy

Heureusement, nous n’avons pas à travailler avec des fonctions constructrices puisque TypeScript fournit le mot-clé class pour créer une classe qui est beaucoup plus facile à travailler qu’une fonction constructrice, croyez-moi. En fait, un class au fond est une fonction constructeur en JavaScript. Essayez l’exemple ci-dessous.

class Animal{
constructor( _name ) {
this.name = _name;
}
}console.log( typeof Animal ); // "function"

Une classe et une fonction constructeur sont une seule et même chose. La seule différence est que la class nous donne une syntaxe OOP riche pour travailler avec. Par conséquent, une interface de type fonction constructrice représente une classe.

(function-signature-with-new.ts)

Dans l’exemple ci-dessus, nous avons défini la classe Animal avec une fonction constructrice qui accepte un argument de type string. Vous pouvez considérer cela comme une fonction constructeur qui a une signature similaire au constructeur Animal.

La AnimalInterface définit une fonction constructeur puisqu’elle a la fonction anonyme préfixée par le mot clé new. Cela signifie que la classe Animal se qualifie pour être un type de AnimalInterface. Ici, le type d’interface AnimalInterface est équivalent au type de fonction new (sound: string) => any.

La fonction createAnimal accepte ctor argument de type AnimalInterface, donc nous pouvons passer la classe Animal comme valeur d’argument. Nous ne pourrons pas ajouter la signature de méthode getSound de la classe Animal dans AnimalInterface et la raison est expliquée dans la leçon Classes.

Types indexables

Un objet indexable est un objet dont les propriétés peuvent être accédées en utilisant une signature d’index comme obj. C’est la façon par défaut d’accéder à un élément de tableau mais nous pouvons également le faire pour l’objet.

var a = ;
var o = { one: 1, two: 2, three: 3 };console.log( a ); // 1
console.log( a ); // 1 (same as `a.one`)

Parfois, votre objet peut avoir un nombre arbitraire de propriétés sans forme définie. Dans ce cas, vous pouvez simplement utiliser le type object. Cependant, ce type object définit toute valeur qui n’est pas number, string, boolean, symbol, null, ou undefined comme discuté dans la leçon de base sur les types.

Si nous devons vérifier strictement si une valeur est un objet JavaScript ordinaire, alors nous pourrions avoir un problème. Cela peut être résolu en utilisant un type d’interface avec une signature d’index pour le nom de la propriété.

interface SimpleObject {
: any;
}

L’interface SimpleObject définit la forme d’un objet avec string clés dont les valeurs peuvent être de type de données any. Ici, le nom de la propriété key est juste utilisé comme placeholder puisqu’il est entre crochets.

(indexable-type-object.ts)

Dans l’exemple ci-dessus, nous avons défini ross et monica objet de type SimpleObject interface. Puisque ces objets contiennent des clés string et des valeurs de type any, c’est parfaitement légal.

Si vous êtes confus au sujet de la clé 1 dans le monica qui est un type de number, c’est légal puisque les éléments d’objet ou de tableau en JavaScript peuvent être indexés en utilisant des clés number ou string, comme indiqué ci-dessous.

var o = { 0: 'Zero', '1': 'One' };
var a = ;console.log( o ); // Zero
console.log( o ); // One
console.log( a ); // One
console.log( a ); // One

Si nous devons être plus précis sur le type de clés et leurs valeurs, nous pouvons sûrement le faire aussi. Par exemple, nous pouvons définir un type d’interface indexable avec des clés de type number et des valeurs de type number si nous le voulons.

💡 Un type de clé de signature d’index doit être soit string soit number.

(indexable-type-number.ts)

Dans l’exemple ci-dessus, nous avons défini une interface LapTimes qui peut contenir des noms de propriétés de type number et des valeurs de type number. Cette interface peut représenter une structure de données qui peut être indexée en utilisant des clés number donc le tableau ross et les objets monica et joey sont légaux.

Cependant, l’objet rachel ne respecte pas la forme de LapTimes puisque la clé one est un string et qu’on ne peut y accéder qu’en utilisant des string comme rachel et rien d’autre. Par conséquent, le compilateur TypeScript lancera une erreur comme indiqué ci-dessus.

Il est possible d’avoir certaines propriétés obligatoires et certaines facultatives dans un type d’interface indexable. Cela peut être très utile lorsque nous avons besoin qu’un objet ait une certaine forme, mais cela n’a pas vraiment d’importance si nous obtenons des propriétés supplémentaires et non désirées dans l’objet.

(indexable-type-required-properties.ts)

Dans l’exemple ci-dessus, nous avons défini une interface LapTimes qui doit contenir la propriété name avec la valeur string et la propriété optionnelle age avec la valeur number. Un objet de type LapTimes peut aussi avoir des propriétés arbitraires dont les clés doivent être number et dont les valeurs doivent aussi être number.

L’objet ross est un objet LapTimes valide même s’il n’a pas la propriété age puisqu’elle est optionnelle. Cependant, monica possède la propriété age mais sa valeur est string donc il n’est pas conforme à l’interface LapTimes.

L’objet joey n’est pas non plus conforme à l’interface LapTimes car il possède une propriété gender qui est un type de string. L’objet rachel n’a pas la propriété name qui est requise dans l’interface LapTimes.

💡 Il y a quelques gotchas auxquels nous devons faire attention en utilisant les types indexables. Ils sont mentionnés dans cette documentation.

Extending Interface

Comme les classes, une interface peut hériter des propriétés d’autres interfaces. Cependant, contrairement aux classes en JavaScript, une interface peut hériter de plusieurs interfaces. Nous utilisons le mot clé extends pour hériter d’une interface.

(extend-interface.ts)

En étendant une interface, l’interface enfant obtient toutes les propriétés de l’interface parent. Ceci est très utile lorsque plusieurs interfaces ont une structure commune et que nous voulons éviter la duplication de code en prenant les propriétés communes dans une interface commune qui peut être héritée plus tard.

(extend-multiple-interfaces.ts)

Dans l’exemple ci-dessus, nous avons créé une interface Student qui hérite des propriétés de l’interface Person et Player.

Déclarations d’interfaces multiples

Dans la section précédente, nous avons appris comment une interface peut hériter des propriétés d’une autre interface. Ceci a été fait en utilisant le mot-clé extend. Cependant, lorsque des interfaces portant le même nom sont déclarées dans le même module (fichier), TypeScript fusionne leurs propriétés ensemble tant qu’elles ont des noms de propriétés distincts ou que leurs types de propriétés conflictuels sont les mêmes.

(merged-interface.ts)

Dans l’exemple ci-dessus, nous avons déclaré l’interface Person plusieurs fois. On obtiendra une seule déclaration d’interface Person en fusionnant les propriétés de toutes les déclarations d’interface Person.

💡 Dans la leçon Classes, nous avons appris qu’une class déclare implicitement une interface et qu’une interface peut étendre cette interface. Ainsi, si un programme a une classe Person et une interface Person, alors le type Person final (interface) aura des propriétés fusionnées entre la classe et l’interface.

Interfaces imbriquées

Une interface peut avoir des structures profondément imbriquées. Dans l’exemple ci-dessous, le champ info de l’interface Student définit la forme d’un objet avec des propriétés firstName et lastName.

(nested-shape.ts)

De même, il est parfaitement légal pour un champ d’une interface d’avoir le type d’une autre interface. Dans l’exemple suivant, le champ info de l’interface Student a le type de l’interface Person.

(nested-interface.ts)

.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.