Mr Josselin A - TypeScript









Types avancés

Références

L'actualité

Librairie

L'information

Types d'intersection

Un type d'intersection combine plusieurs types en un. Cela vous permet d'ajouter des types existants pour obtenir un type unique doté de toutes les fonctionnalités dont vous avez besoin. Par exemple, Person & Serializable & Loggable est une personne et Serializable et Loggable. Cela signifie qu'un objet de ce type aura tous les membres des trois types.

Vous verrez principalement les types d'intersection utilisés pour les mixin et d'autres concepts qui ne rentrent pas dans le moule classique orienté objet. ( Il y en a beaucoup en JavaScript ! ).

Voici un exemple simple qui montre comment créer un mixin :


Types d'Union

Les types d'union sont étroitement liés aux types d'intersection, mais ils sont utilisés très différemment. Parfois, vous rencontrez une bibliothèque qui s'attend à ce qu'un paramètre soit un number ou un string.

Par exemple, prenez la fonction suivante :

Le problème avec padLeft est que son paramètre padding est tapé comme any. Cela signifie que nous pouvons l'appeler avec un argument qui n'est ni un number ni un string, mais TypeScript l'acceptera.

Dans le code traditionnel orienté objet, nous pouvons faire abstraction des deux types en créant une hiérarchie de types. Bien que cela soit beaucoup plus explicite, c'est aussi un peu exagéré. L'un des avantages de la version originale de padLeft est que nous n'avons pu passer que des primitives. Cela signifiait que l'utilisation était simple et concise. Cette nouvelle approche ne nous aiderait pas non plus si nous essayions simplement d'utiliser une fonction qui existe déjà ailleurs.

Au lieu de any, nous pouvons utiliser un type d'union pour le paramètre padding :

Un type d'union décrit une valeur qui peut être un type parmi plusieurs. Nous utilisons la barre verticale (|) pour séparer chaque type, de même que number | string | boolean le type d'une valeur qui peut être a number, a string ou a boolean.

Si nous avons une valeur qui a un type d'union, nous ne pouvons accéder qu'aux membres qui sont communs à tous les types de l'union.

Les types d'union peuvent être un peu délicats ici, mais il faut un peu d'intuition pour s'y habituer. Si une valeur a de type A | B, nous savons seulement avec certitude qu'elle a des membres qui ont tous les deux A et B. Dans cet exemple, Bird est membre est nommé fly. Nous ne pouvons pas être sûrs qu'une variable typée Bird | Fish a une méthode fly. Si la variable est vraiment un Fish à l'exécution, l'appel pet.fly() échouera.

Type Gardes et types différenciant

Les types d'union sont utiles pour modéliser des situations où les valeurs peuvent se chevaucher dans les types qu'elles peuvent prendre. Que se passe-t-il lorsque nous devons savoir précisément si nous en avons un Fish ? Un langage courant JavaScript pour différencier deux valeurs possibles la solution est de vérifier la présence d'un membre. Comme nous l'avons mentionné, vous ne pouvez accéder qu'aux membres dont il est garanti qu'ils appartiennent à tous les groupes constitutifs d'un type syndical.

Pour que le même code fonctionne, nous devons utiliser une assertion de type :

Gardes de type défini par l'utilisateur

Notez que nous avons dû utiliser plusieurs fois des assertions de type. Il serait bien mieux qu'une fois la vérification effectuée, nous puissions connaître le type de chaque branche pet.

Il se trouve que TypeScript a quelque chose appelé un type guard. Un type guard est une expression qui effectue une vérification à l'exécution qui garantit le type dans une certaine portée. Pour définir une protection de type, nous devons simplement définir une fonction dont le type de retour est un prédicat de type :

pet is Fish est notre prédicat de type dans cet exemple. Un prédicat prend la forme parameterName is Type, où parameterName doit être le nom d'un paramètre de la signature de la fonction actuelle.

À tout moment isFish appelé avec une variable, TypeScript restreindra cette variable à ce type spécifique si le type d'origine est compatible.

Notez que TypeScript non seulement sait que pet est un Fish dans la branche; il sait aussi que dans la branche else, vous n'avez un Fish, vous devez donc un Bird

typeof type gardes

Revenons en arrière et écrivons le code de la version padLeft qui utilise des types d'union. Nous pourrions l'écrire avec les prédicats de type comme suit :

Cependant, avoir à définir une fonction pour déterminer si un type est une primitive est un peu pénible. Heureusement, vous n'avez pas besoin de résumer typeof x === "number" dans sa propre fonction car TypeScript le reconnaîtra comme un garde de type. Cela signifie que nous pourrions simplement écrire ces chèques en ligne.

Ces gardes de type typeof sont reconnus sous deux formes différentes : typeof v === "typename" et typeof v !== "typename""typename" doit être "number", "string", "boolean" ou "symbol". Bien que TypeScript ne vous empêche pas de comparer d'autres chaînes, le langage ne reconnaît pas ces expressions en tant que gardes du type.

instanceof type gardes

Si vous avez lu des informations sur typeof les polices de caractères et connaissez instanceof de l'opérateur JavaScript, vous avez probablement une idée de ce à quoi sert cette section.

Les gardes de types instanceof permettent de réduire les types à l'aide de leur fonction constructeur. Prenons, par exemple, notre exemple de dépisteur de cordes industriel :

Le côté droit de la fonction instanceof doit être une fonction constructeur, et TypeScript se limitera à :

  • le type de la propriété prototype de la fonction si son type n'est pas any
  • l'union des types retournés par les signatures de construction de ce type

dans cet ordre.

Types nullables

TypeScript a deux types spéciaux, null et undefined, qui ont respectivement les valeurs null et indéfinie. Nous en avons parlé brièvement dans la section Types de base. Par défaut, le vérificateur de type considère null et peut être undefined assigné à quoi que ce soit. Effectivement, null et undefined sont des valeurs valides de chaque type. Cela signifie qu'il n'est pas possible d'empêcher leur assignation à quelque type que ce soit, même si vous souhaitez l'empêcher. L'inventeur de null Tony Hoare appelle cela "son erreur d'un milliard de dollars".

Le drapeau --strictNullChecks corrige ceci : lorsque vous déclarez une variable, elle n'inclut pas automatiquement null ni undefined. Vous pouvez les inclure explicitement en utilisant un type d'union :

Notez que TypeScript traite null et undefined différemment afin de correspondre à la sémantique JavaScript. string | null est un type différent de string | undefined et string | undefined | null.

Paramètres optionnels et propriétés

Avec --strictNullChecks, un paramètre optionnel ajoute automatiquement | undefined :

Il en va de même pour les propriétés facultatives :


Gardes de type et assertions de type

Comme les types nullables sont implémentés avec une union, vous devez utiliser un type guard pour vous en débarrasser null. Heureusement, c'est le même code que vous écriviez en JavaScript :

L'élimination null est assez évidente ici, mais vous pouvez aussi utiliser des opérateurs de terser :

Dans les cas où le compilateur ne peut pas éliminer null ou undefined, vous pouvez utiliser l'opérateur d'assertion de type pour les supprimer manuellement. La syntaxe est postfix ! : identifier! supprime null et undefined du type de identifier :

L'exemple utilise ici une fonction imbriquée car le compilateur ne peut pas éliminer les valeurs NULL dans une fonction imbriquée (à l'exception des expressions de fonction invoquées immédiatement). En effet, il ne peut pas suivre tous les appels de la fonction imbriquée, surtout si vous le renvoyez à partir de la fonction externe. Sans savoir où la fonction est appelée, il ne peut pas savoir quel type de namesera au moment de l'exécution du corps.

Type Alias

Les alias de type créent un nouveau nom pour un type. Les alias de types ressemblent parfois aux interfaces, mais peuvent nommer des primitives, des unions, des n-uplets et tout autre type que vous auriez sinon dû écrire manuellement.

L'aliasing ne crée pas réellement un nouveau type; il crée un nouveau nom pour faire référence à ce type. L'aliasing d'une primitive n'est pas très utile, bien que cela puisse être utilisé comme une forme de documentation.

Tout comme les interfaces, les alias de types peuvent également être génériques. Nous pouvons simplement ajouter des paramètres de type et les utiliser à droite de la déclaration d'alias :

Nous pouvons également avoir un alias de type qui se réfère à lui-même dans une propriété :

Avec les types d'intersection, nous pouvons créer des types assez hallucinants :

Cependant, il n'est pas possible qu'un alias de type apparaisse ailleurs sur le côté droit de la déclaration :

Interfaces et alias de types

Comme nous l'avons mentionné, les alias de type peuvent agir comme des interfaces. Cependant, il existe des différences subtiles.

Une différence est que les interfaces créent un nouveau nom qui est utilisé partout. Les alias de type ne créent pas de nouveau nom. Par exemple, les messages d'erreur n'utilisent pas le nom d'alias.

Dans le code ci-dessous, survoler interfaced dans un éditeur montrera qu'il retourne un Interface, mais montrera que aliased retourne le type littéral d'objet.

Une autre différence plus importante est que les alias de types ne peuvent pas être étendus ou implémentés (ils ne peuvent pas non plus étendre / implémenter d'autres types). étant donné qu'une propriété idéale du logiciel est ouverte à l'extension, vous devez toujours utiliser une interface sur un alias de type, si possible.

D'autre part, si vous ne pouvez pas exprimer une forme avec une interface et que vous devez utiliser un type d'union ou de tuple, les alias de type sont généralement la solution.

Types littéraux de chaîne

Les types littéraux de chaîne vous permettent de spécifier la valeur exacte qu'une chaîne doit avoir. En pratique, les types littéraux de chaîne se combinent parfaitement avec les types d'union, les gardes de type et les alias de type. Vous pouvez utiliser ces fonctionnalités ensemble pour obtenir un comportement semblable à une énumération avec des chaînes.

Vous pouvez passer n'importe laquelle des trois chaînes autorisées, mais toute autre chaîne donnera l'erreur

Les types littéraux de chaîne peuvent être utilisés de la même manière pour distinguer les surcharges :


Types littéraux numériques

TypeScript a aussi des types littéraux numériques.

Celles-ci sont rarement écrites explicitement, elles peuvent être utiles lorsque le rétrécissement peut attraper des bogues :

En d'autres termes, x doit être 1 quand il est comparé à 2, ce qui signifie que la vérification ci-dessus fait une comparaison invalide.

Types de membres Enum

Comme mentionné dans notre section sur les énumérations, les membres d'énum ont des types lorsque chaque membre est initialisé littéralement.

La plupart du temps, lorsque nous parlons de "types singleton", nous faisons référence à la fois aux types de membre enum et aux types littéraux numériques / chaînes, même si de nombreux utilisateurs utiliseront indifféremment les "types singleton" et les "types littéraux".

Syndicats discriminés

Vous pouvez combiner des types singleton, des types d'union, des gardes de type et des alias de type pour créer un modèle avancé appelé unions discriminées, également appelé unions étiquetées ou types de données algébriques. Les unions discriminées sont utiles dans la programmation fonctionnelle. Certaines langues discriminent automatiquement les syndicats pour vous. Au lieu de cela, TypeScript s'appuie sur les modèles JavaScript tels qu'ils existent aujourd'hui.

Il y a trois ingrédients :

  • Types qui ont une propriété commune commune, singleton - le discriminant.
  • Un alias de type qui prend l'union de ces types - l'union.
  • Type guards sur la propriété commune.


Nous déclarons d'abord les interfaces que nous unirons. Chaque interface a une propriété kind avec un type de littéral de chaîne différent. La propriété kind s'appelle le discriminant ou l'étiquette. Les autres propriétés sont spécifiques à chaque interface. Notez que les interfaces ne sont actuellement pas liées.

Mettons-les dans un syndicat :


Utilisons maintenant l'union discriminée :

Vérification d'exhaustivité

Nous aimerions que le compilateur nous dise quand nous ne couvrons pas toutes les variantes du syndicat discriminé. Par exemple, si nous ajoutons Triangle à Shape, nous devons également mettre à jour area :

Il y a deux façons de faire ça. La première consiste à activer --strictNullChecks et à spécifier un type de retour :

Comme le switch n'est plus exhaustif, TypeScript est conscient que la fonction peut parfois être retournée undefined. Si vous avez un type de retour explicite number, vous obtiendrez une erreur indiquant que le type de retour est réellement number | undefined. Cependant, cette méthode est assez subtile et --strictNullChecks ne fonctionne pas toujours avec l'ancien code.

La deuxième méthode utilise le type never, utilisé par le compilateur pour vérifier l'exhaustivité :

Ici, les assertNever contrôles s de type never, le type qui reste après la suppression de tous les autres cas. Si vous oubliez un cas, vous saurez un type réel et vous obtiendrez une erreur de type. Cette méthode nécessite de définir une fonction supplémentaire, mais c'est beaucoup plus évident quand vous l'oubliez.

this Types polymorphes

Un type this polymorphe représente un type qui est le sous type de la classe ou de l'interface qui le contient. Ceci est appelé polymorphisme lié à F. Cela rend les interfaces fluides hirarchiques beaucoup plus faciles à exprimer, par exemple. Prenons une simple calculatrice qui retourne this après chaque opération :
Comme la classe utilise des types this, vous pouvez l'étendre et la nouvelle classe peut utiliser les anciennes méthodes sans modification.
Sans types this, ScientificCalculator n'aurait pas été en mesure d'étendre BasicCalculator et de maintenir l'interface fluide. multiply serait retourné BasicCalculator, qui n'a pas la méthode sin. Cependant, avec les types this, les retours multiply this, ce qui est ScientificCalculator ici.

Types d'index

Avec les types d'index, vous pouvez demander au compilateur de vérifier le code qui utilise des noms de propriétés dynamiques. Par exemple, un modèle Javascript courant consiste à choisir un sous-ensemble de propriétés dans un objet :

Voici comment écrire et utiliser cette fonction dans TypeScript, à l'aide de la requête de type d'index et des opérateurs d'accès indexés :

Le compilateur vérifie que name s'agit bien d'une propriété Person. L'exemple introduit quelques nouveaux opérateurs de type. Le premier est keyof T l'opérateur de requête de type index. Pour tout type T, keyof T est l'union des noms de propriété publique connus de T.

Par exemple :

keyof Person est complètement interchangeable avec 'name' | 'age'. La différence est que si vous ajoutez une autre propriété à Person, par exemple address : string, elle keyof Person sera automatiquement mise à jour 'name' | 'age' | 'address'. Et vous pouvez utiliser keyof des contextes génériques tels que pluck, où vous ne pouvez pas connaître les noms de propriété à l'avance. Cela signifie que le compilateur vérifiera que vous transmettez le bon ensemble de noms de propriétés à pluck :

Le deuxième opérateur est T[K] l'opérateur d'accès indexé. Ici, la syntaxe de type reflète la syntaxe d'expression. Cela signifie que cela person['name'] a le type Person['name'] ce qui dans notre exemple est juste string. Cependant, tout comme les requêtes de type index, vous pouvez utiliser T[K] un contexte générique, où son pouvoir réel prend vie. Vous devez juste vous assurer que le type variable K extends keyof T. Voici un autre exemple avec une fonction nommée getProperty.

Dans getProperty, o: T et name: K, donc ça veut dire o[name]: T[K]. Une fois que vous avez renvoyé le résultat T[K], le compilateur instancie le type actuel de la clé. Le type de retour getProperty varie en fonction de la propriété que vous demandez.

Types d'index et signatures d'index de chaîne

keyof et T[K] interagir avec les signatures d'index de chaîne. Si vous avez un type avec une signature d'index de chaîne, ce keyof T sera juste string. Et T[string] est juste le type de la signature d'index :


Types mappés

Une tâche courante consiste à prendre un type existant et à rendre chacune de ses propriétés facultative :

Ou nous pourrions vouloir une version en lecture seule :

Cela se produit assez souvent en Javascript que TypeScript offre un moyen de créer de nouveaux types basés sur d'anciens types - types mappés. Dans un type mappé, le nouveau type transforme chaque propriété de l'ancien type de la même manière. Par exemple, vous pouvez rendre toutes les propriétés d'un type readonly ou facultatives.

Voici quelques exemples :

Et pour l'utiliser :

Jetons un coup d'oeil au type mappé le plus simple et à ses parties :

La syntaxe ressemble à la syntaxe des signatures d'index avec un for .. in intérieur. Il y a trois parties :

  • La variable type K, qui est liée à chaque propriété.
  • L'union littérale de chaîne Keys, qui contient les noms des propriétés à itérer.
  • Le type résultant de la propriété.

Dans cet exemple simple, Keys est une liste codée en dur de noms de propriétés et le type de propriété est toujours boolean, de sorte que ce type mappé est équivalent à l'écriture :

Les applications réelles, cependant, ressemblent Readonly ou Partial dépassent. Ils sont basés sur un type existant et transforment les propriétés d'une manière ou d'une autre. C'est là que keyof interviennent les types d'accès indexés :

Mais il est plus utile d'avoir une version générale.

Dans ces exemples, la liste des propriétés est keyof T et le type résultant est une variante de T[P]. C'est un bon modèle pour toute utilisation générale des types mappés. En effet, ce type de transformation est homomorphique, ce qui signifie que le mappage s'applique uniquement aux propriétés des T autres propriétés. Le compilateur sait qu'il peut copier tous les modificateurs de propriétés existants avant d'en ajouter de nouveaux. Par exemple, si Person.name était en lecture seule, Partial‹Person›.name serait en lecture seule et facultatif.

Voici un autre exemple, dans lequel T[P] est encapsulé dans une classe Proxy‹T› :

Notez que Readonly‹T› et Partial‹T› sont très utiles, ils sont inclus dans la bibliothèque standard de TypeScript avec Picket Record :

Readonly, Partial et Pick sont homomorphic alors que Record ne l'est pas. Un indice qui Record n'est pas homomorphe est qu'il ne faut pas un type d'entrée pour copier les propriétés de :

Les types non homomorphes créent essentiellement de nouvelles propriétés, ils ne peuvent donc pas copier les modificateurs de propriété de n'importe où.

Inférence à partir de types mappés

Maintenant que vous savez comment encapsuler les propriétés d'un type, vous devez ensuite les décompresser. Heureusement, c'est assez facile :

Notez que cette inférence de déballage ne fonctionne que sur les types mappés homomorphes. Si le type mappé n'est pas homomorphe, vous devrez donner un paramètre de type explicite à votre fonction de décompression.

Types conditionnels

TypeScript 2.8 introduit des types conditionnels qui permettent d'exprimer des mappages de types non uniformes. Un type conditionnel sélectionne l'un des deux types possibles en fonction d'une condition exprimée sous la forme d'un test de relation de type :

Le type ci-dessus signifie quand T est assignable au U type est X, sinon le type est Y.

Un type conditionnel T extends U ? X : Y est résolu en X ou Y, ou différé car la condition dépend d'une ou de plusieurs variables de type. Lorsque T ou U contient des variables de type, la résolution en X ou Y, ou en différé, est déterminée par le fait que le système de types possède suffisamment d'informations pour conclure qu'il T est toujours assignable U.

À titre d'exemple de types immédiatement résolus, examinons l'exemple suivant :

Un autre exemple serait l'alias TypeName de type, qui utilise des types conditionnels imbriqués :

Mais comme exemple d'endroit où les types conditionnels sont différés - où ils restent au lieu de choisir une branche - seraient les suivants :

Dans ce qui précède, la variable a a un type conditionnel qui n'a pas encore choisi de branche. Quand un autre morceau de code finit par appeler foo, il sera remplacé U par un autre type et TypeScript réévaluera le type conditionnel, en décidant s'il peut réellement choisir une branche.

En attendant, nous pouvons affecter un type conditionnel à tout autre type de cible tant que chaque branche de la condition est assignable à cette cible. Ainsi, dans notre exemple ci-dessus, nous avons pu affecter U extends Foo ? string : number à, string | number peu importe ce que le conditionnel évalue, il est connu pour être l'un string ou l'autre number.

Types conditionnels distributifs

Les types conditionnels dans lesquels le type vérifié est un paramètre de type nu sont appelés types conditionnels distributifs. Les types conditionnels distributifs sont automatiquement répartis sur les types d'union lors de l'instanciation. Par exemple, une instanciation de T extends U ? X : Y avec l'argument type A | B | C pour T est résolue en tant que (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

Exemple :

Dans les instanciations d'un type conditionnel distributif T extends U ? X : Y, les références au T type conditionnel sont résolues en constituants individuels du type union (c'est-à- dire, T fait référence aux constituants individuels une fois que le type conditionnel est distribué sur le type union). De plus, les références à T inside X ont une contrainte de paramètre de type supplémentaire U (c'est-à-dire T qu'elles sont assignables à U dedans X).

Exemple :

Avis qui T a la contrainte supplémentaire any[] dans la branche vraie de Boxed‹T› et il est donc possible de se référer au type d'élément du tableau comme T[number]. Notez également comment le type conditionnel est distribué sur le type d'union dans le dernier exemple.

La propriété distributive des types conditionnels peut être utilisée pour filtrer les types d'union :

Les types conditionnels sont particulièrement utiles lorsqu'ils sont combinés avec des types mappés :

Semblables aux types d'union et d'intersection, les types conditionnels ne sont pas autorisés à se référencer eux-mêmes de manière récursive. Par exemple, ce qui suit est une erreur.

Exemple :

Inférence de type dans les types conditionnels

Dans la clause extends d'un type conditionnel, il est maintenant possible d'avoir des déclarations qui introduisent une variable de type à inférer. De telles variables de type inféré peuvent être référencées dans la branche vraie du type conditionnel. Il est possible d'avoir plusieurs inferemplacements pour la même variable de type.

Par exemple, ce qui suit extrait le type de retour d'un type de fonction :

Les types conditionnels peuvent être imbriqués pour former une séquence de correspondances de modèle qui sont évaluées dans l'ordre :

L'exemple suivant montre comment plusieurs candidats pour la même variable de type dans des positions de co-variant provoquent l'inférence d'un type d'union :

De même, plusieurs candidats pour la même variable de type dans des positions contra-variables entraînent l'inférence d'un type d'intersection :

Lors d'une inférence à partir d'un type avec plusieurs signatures d'appel (comme le type d'une fonction surchargée), les inférences sont faites à partir de la dernière signature (qui est vraisemblablement le cas le plus permissif fourre-tout). Il n'est pas possible d'effectuer une résolution de surcharge basée sur une liste de types d'arguments.

Il n'est pas possible d'utiliser des déclarations inféré dans les clauses de contrainte pour les paramètres de type standard :

Cependant, le même effet peut être obtenu en effaçant les variables de type dans la contrainte et en spécifiant un type conditionnel :

Types conditionnels prédéfinis

TypeScript 2.8 ajoute plusieurs types conditionnels prédéfinis à lib.d.ts:

Exclude‹T, U› - Exclure de T ces types qui sont assignables à U.
Extract‹T, U› - Extrait de T ces types qui sont assignables à U.
NonNullable‹T› - Exclure null et undefined de T.
ReturnType‹T› - Obtenir le type de retour d'un type de fonction.
InstanceType‹T› - Obtenir le type d'instance d'un type de fonction constructeur.

Exemple :

Le type Exclude correspond à une implémentation appropriée du type Diff suggéré ici. Nous avons utilisé le nom Exclude pour éviter de casser le code existant qui définit un Diff, et nous pensons que ce nom véhicule mieux la sémantique du type. Nous n'avons pas inclus le Omit‹T, K› type car il est écrit trivialement comme Pick‹T, Exclude‹keyof T, K››.