Manuscrit. Types avancés

image



Bonjour les Habitants! Nous avons soumis une autre nouveauté

" Professional TypeScript. Développement d'applications JavaScript évolutives " à l'imprimerie . Dans ce livre, les programmeurs déjà intermédiaires en JavaScript apprendront à maîtriser TypeScript. Vous verrez comment TypeScript peut vous aider à redimensionner votre code 10 fois mieux et à rendre la programmation amusante à nouveau.



Ci-dessous, un extrait d'un chapitre du livre "Advanced Types".



Types avancés



Le système de type TypeScript de renommée mondiale surprend même les programmeurs Haskell par ses capacités. Comme vous le savez déjà, il est non seulement expressif, mais également facile à utiliser: les restrictions de type et les relations qu'il contient sont concises, compréhensibles et, dans la plupart des cas, sont déduites automatiquement.



La modélisation d'éléments JavaScript dynamiques tels que les prototypes, liés par cela, les surcharges de fonctions et les objets en constante évolution nécessite un système de types et leurs opérateurs aussi riches que Batman le prendrait.



Je commencerai ce chapitre par une plongée approfondie dans les sujets du sous-typage, de la compatibilité, de la variance, des variables aléatoires et de l'extension. Ensuite, je développerai les spécificités de la vérification de type basée sur le flux, y compris le raffinement et la totalité. Ensuite, je vais démontrer certaines fonctionnalités de programmation avancées au niveau du type: connexion et mappage de types d'objets, utilisation de types conditionnels, définition de protections de type et solutions de secours telles que les assertions de type et les assertions d'affectation explicites. Enfin, je vais vous présenter quelques modèles avancés pour améliorer la sécurité des types: création de modèles d'objets compagnons, améliorations d'interface pour les tuples, imitation de types nominaux et extension de prototype sûre.



Relations entre les types



Examinons de plus près les relations dans TypeScript.



Sous-types et supertypes



Nous avons déjà abordé la compatibilité dans la section À propos des types à la p. 34, nous allons donc plonger directement dans ce sujet, en commençant par définir le sous-type.

image




Revenez à la fig. 3.1 et voir les associations de sous-types intégrées de TypeScript.

image




  • Array est un sous-type d'un objet.
  • Un tuple est un sous-type d'un tableau.
  • Tout est un sous-type de tout.
  • n'est jamais un sous-type de tout.
  • La classe Bird, qui étend la classe Animal, est un sous-type de la classe Animal.


Selon la définition que je viens de donner pour un sous-type, cela signifie que:



  • Partout où vous avez besoin d'un objet, vous pouvez utiliser un tableau.
  • Partout où un tableau est nécessaire, un tuple peut être utilisé.
  • Partout où vous en avez besoin, vous pouvez utiliser un objet.
  • Ne peut jamais être utilisé partout.
  • Partout où vous avez besoin d'un animal, vous pouvez utiliser Bird.


Un supertype est l'opposé d'un sous-type.



SUPERTYPE



Si vous avez deux types, A et B, et que B est un supertype de A, vous pouvez utiliser A en toute sécurité partout où B est requis (Figure 6.2).


image


Et encore une fois, sur la base du diagramme de la Fig. 3.1:



  • Array est un supertype de tuple.
  • L'objet est un supertype de tableau.
  • Any est un supertype de tout.
  • Ce n'est jamais le super-type de personne.
  • L'animal est un supertype d'oiseau.


C'est juste l'opposé des sous-types et rien de plus.



Variation



Pour la plupart des types, il est assez facile de comprendre si un certain type A est un sous-type de B. Pour les types simples tels que nombre, chaîne, etc., vous pouvez vous référer au diagramme de la Fig. 3.1 ou déterminer indépendamment que le nombre contenu dans le numéro d'union | string est un sous-type de cette union.



Mais il existe des types plus complexes, tels que les génériques. Considérez ces questions:



  • Quand Array <A> est-il un sous-type de Array <B>?
  • Quand la forme A est-elle un sous-type de la forme B?
  • Quand la fonction (a: A) => B est-elle un sous-type de fonction (c: C) => D?


Les règles de sous-typage pour les types contenant d'autres types (c'est-à-dire, ayant des paramètres de type comme Array <A>, des formulaires avec des champs comme {a: number} ou des fonctions comme (a: A) => B) sont plus difficiles à comprendre, car elles ne le sont pas cohérent entre les différents langages de programmation.



Pour faciliter la lecture des règles suivantes, je vais présenter quelques éléments de syntaxe qui ne fonctionnent pas dans TypeScript (ne vous inquiétez pas, ce n'est pas mathématique):



  • A <: B signifie "A est un sous-type identique au type B";
  • A>: B signifie "A est un supertype du même type que le type B".


Variation du formulaire et du tableau



Pour comprendre pourquoi les langues ne sont pas d'accord dans les règles de sous-typage des types complexes, un exemple avec un formulaire qui décrit un utilisateur dans une application vous aidera. Nous le représentons à travers quelques types:



//  ,   .
type ExistingUser = {
    id: number
   name: string
}
//  ,     .
type NewUser = {
   name: string
}


Supposons qu'un stagiaire de votre entreprise soit chargé d'écrire du code pour supprimer un utilisateur. Il commence par ce qui suit:



function deleteUser(user: {id?: number, name: string}) {
    delete user.id
}
let existingUser: ExistingUser = {
    id: 123456,
    name: 'Ima User'
}
deleteUser(existingUser)


deleteUser reçoit un objet de type {id?: number, name: string} et lui transmet un utilisateur existant de type {id: number, name: string}. Notez que le type de la propriété id (number) est un sous-type du type attendu (number | undefined). Par conséquent, tout l'objet {id: number, name: string} est un sous-type de {id?: Number, name: string}, donc TypeScript le permet.



Voyez-vous des problèmes de sécurité? Il y en a un: après avoir passé ExistingUser à deleteUser, TypeScript ne sait pas que l'ID utilisateur a été supprimé, donc si vous lisez existingUser.id après avoir supprimé deleteUser (existingUser), alors TypeScript supposera toujours que existingUser.id est de type number.



De toute évidence, l'utilisation d'un type d'objet où son supertype est attendu n'est pas sûre. Alors pourquoi TypeScript permet-il cela? L'essentiel est que ce n'était pas censé être totalement sûr. Son système de type cherche à détecter les vraies erreurs et à les rendre visibles aux programmeurs de tous niveaux. Étant donné que les mises à jour destructives (comme la suppression d'une propriété) sont relativement rares dans la pratique, TypeScript est détendu et vous permet d'attribuer un objet là où son supertype est attendu.



Et que dire du cas contraire: est-il possible d'attribuer un objet là où son sous-type est attendu?



Ajoutons un nouveau type pour l'ancien utilisateur, puis supprimons l'utilisateur avec ce type (imaginez ajouter des types au code que votre collègue a écrit):



type LegacyUser = {
    id?: number | string
    name: string
}
let legacyUser: LegacyUser = {
    id: '793331',
    name: 'Xin Yang'
}
deleteUser(legacyUser) //  TS2345: a  'LegacyUser'
                                  //    
                                  // '{id?: number |undefined, name: string}'.
                                 //  'string'    'number |
                                 // undefined'.


Lorsque vous soumettez un formulaire avec une propriété dont le type est un supertype du type attendu, TypeScript jure. C'est parce que id est une chaîne | nombre | undefined et deleteUser ne gère que le cas où id est un nombre | indéfini.



Lorsque vous attendez un formulaire, vous pouvez transmettre un type avec des types de propriété qui sont <: des types attendus, mais vous ne pouvez pas transmettre un formulaire sans types de propriété qui sont des supertypes de leurs types attendus. Lorsque nous parlons de types, nous disons: "Les formes TypeScript (objets et classes) sont covariantes dans les types de leurs propriétés. Autrement dit, pour que l'objet A soit affecté à l'objet B, chacune de ses propriétés doit être <: la propriété correspondante dans B. La



covariance est l'un des quatre types de variance:



Invariance En

particulier, T est nécessaire.

Covariance

Nécessaire <: T.

Contravariance

nécessaire>: T.

La bivariance

conviendra à <: T ou>: T.



Dans TypeScript, chaque type complexe est covariant dans ses membres - objets, classes, tableaux et types de retour de fonction - à une exception près: les types de paramètres de fonction sont contravariants.



. , . ( ). , Scala, Kotlin Flow, , .



TypeScript : , , , (, id deleteUser, , , ).


Variation de la fonction



Prenons quelques exemples.



La fonction A est un sous-type de fonction B si A a la même arité (nombre de paramètres) ou moins que B, et:



  1. Le type this, appartenant à A, est soit indéfini, soit>: du type this, appartenant à B.
  2. Chacun des paramètres A>: le paramètre correspondant en B.
  3. Type de retour A <: type de retour B.


Notez que pour que la fonction A soit un sous-type de la fonction B, son type et ses paramètres doivent être>: équivalents dans B, tandis que son type de retour doit être <:. Pourquoi la condition s'inverse-t-elle? Pourquoi la simple condition <: ne fonctionne-t-elle pas pour chaque composant (de type this, types de paramètres et type de retour), comme c'est le cas avec les objets, les tableaux, les unions, etc.?



Commençons par définir trois types (au lieu de la classe, vous pouvez utiliser d'autres types, où A: <B <: C):



class Animal {}
class Bird extends Animal {
    chirp() {}
}
class Crow extends Bird {
    caw() {}
}


So Crow <: Bird <: Animal.



Définissons une fonction qui prend Bird et le fait tweeter:



function chirp(bird: Bird): Bird {
    bird.chirp()
    return bird
}


Jusqu'ici tout va bien. Qu'est-ce que TypeScript vous permet de diriger vers le gazouillis?



chirp(new Animal) //  TS2345:   'Animal'
chirp(new Bird) //     'Bird'.
chirp(new Crow)


Une instance Bird (en tant que paramètre chirp de type bird) ou une instance Crow (en tant que sous-type de Bird). Le passage de sous-type fonctionne comme prévu.



Créons une nouvelle fonction. Cette fois, son paramètre sera une fonction:



function clone(f: (b: Bird) => Bird): void {
    // ...
}


clone nécessite une fonction f qui prend Bird et renvoie Bird. Quels types de fonctions peuvent être transmis à f en toute sécurité? Evidemment, la fonction qui reçoit et renvoie Bird:



function birdToBird(b: Bird): Bird {
    // ...
}
clone(birdToBird) // OK


Qu'en est-il d'une fonction qui prend un oiseau mais renvoie un corbeau ou un animal?



function birdToCrow(d: Bird): Crow {
    // ...
}
clone(birdToCrow) // OK
function birdToAnimal(d: Bird): Animal {
    // ...
}
clone(birdToAnimal) //  TS2345:   '(d: Bird) =>
                             // Animal'    
                            // '(b: Bird) => Bird'. 'Animal'
                           //    'Bird'.


birdToCrow fonctionne comme prévu, mais birdToAnimal renvoie une erreur. Pourquoi? Imaginez qu'une implémentation de clone ressemble à ceci:



function clone(f: (b: Bird) => Bird): void {
    let parent = new Bird
    let babyBird = f(parent)
    babyBird.chirp()
}


En passant la fonction f à clone, qui renvoie Animal, nous ne pouvons pas appeler .chirp dedans. Par conséquent, TypeScript doit s'assurer que la fonction que nous transmettons renvoie au moins Bird.



Lorsque nous disons que les fonctions sont covariantes dans leurs types de retour, cela signifie qu'une fonction ne peut être un sous-type d'une autre fonction que si son type de retour est <: le type de retour de cette fonction.



D'accord, qu'en est-il des types de paramètres?



function animalToBird(a: Animal): Bird {
  // ...
}
clone(animalToBird) // OK
function crowToBird(c: Crow): Bird {
  // ...
}
clone(crowToBird)        //  TS2345:   '(c: Crow) =>
                        // Bird'     '
                       // (b: Bird) => Bird'.


Pour qu'une fonction soit compatible avec une autre fonction, tous ses types de paramètres (y compris celui-ci) doivent être>: leurs paramètres correspondants dans l'autre fonction. Pour comprendre pourquoi, pensez à la manière dont l'utilisateur pourrait implémenter crowToBird avant de le passer au clonage?



function crowToBird(c: Crow): Bird {
  c.caw()
  return new Bird
}


TSC-: STRICTFUNCTIONTYPES



- TypeScript this. , , {«strictFunctionTypes»: true} tsconfig.json.



{«strict»: true}, .


Maintenant, si le clone appelle crowToBird avec new Bird, nous obtiendrons une exception, car .caw est défini dans tous les Crow mais pas dans tous les Birds.



Cela signifie que les fonctions sont contravariantes dans leurs paramètres et ce type. Autrement dit, une fonction ne peut être un sous-type d'une autre fonction que si chacun de ses paramètres et de son type est>: leurs paramètres correspondants dans l'autre fonction.



Heureusement, ces règles n'ont pas besoin d'être mémorisées. Souvenez-vous-en simplement lorsque l'éditeur met un trait de soulignement rouge lorsque vous passez une fonction mal tapée quelque part.



Compatibilité



Les relations de sous-type et de supertype sont un concept clé dans tout langage à typage statique. Ils sont également importants pour comprendre le fonctionnement de la compatibilité (rappelez-vous que la compatibilité fait référence aux règles TypeScript régissant l'utilisation du type A lorsque le type B est requis).



Lorsque TypeScript doit répondre à la question "Le type A est-il compatible avec le type B?", Il suit des règles simples. Pour les types non enum - comme les tableaux, les booléens, les nombres, les objets, les fonctions, les classes, les instances de classe et les chaînes, y compris les types littéraux - A est compatible avec B si l'une des conditions est vraie.



  1. Un <: B.
  2. A est n'importe lequel.


La règle 1 n'est qu'une définition de sous-type: si A est un sous-type de B, alors partout où B est nécessaire, vous pouvez utiliser A. La



règle 2 est une exception à la règle 1 pour faciliter l'interaction avec le code JavaScript.

Pour les types d'énumération créés par les mots clés enum ou const enum, le type A est compatible avec l'énumération B si l'une des conditions est vraie.



  1. A est membre de l'énumération B.
  2. B a au moins un membre de type number et A est number.


La règle 1 est exactement la même que pour les types simples (si A est membre de B, alors A est de type B et on dit B <: B).



La règle 2 est nécessaire pour la commodité de travailler avec des énumérations, qui compromettent sérieusement la sécurité de TypeScript (voir la sous-section «Enum» à la page 60), et je recommande de les éviter.



Expansion de type L'expansion de type



est la clé pour comprendre le fonctionnement de l'inférence de type. TypeScript est indulgent dans l'exécution et est plus susceptible de se tromper en déduisant un type plus général qu'en déduisant aussi spécifique que possible. Cela vous facilitera la vie et réduira le temps nécessaire pour traiter les notes du vérificateur de type.



Au chapitre 3, vous avez déjà vu plusieurs exemples d'expansion de type. Considérez les autres.



Lorsque vous déclarez une variable comme mutable (avec let ou var), son type passe du type valeur de son littéral au type de base auquel appartient le littéral:



let a = 'x' // string
let b = 3   // number
var c = true   // boolean
const d = {x: 3}   // {x: number}
enum E {X, Y, Z}
let e = E.X   // E


Cela ne s'applique pas aux déclarations immuables:



const a = 'x' // 'x'
const b = 3   // 3
const c = true   // true
enum E {X, Y, Z}
const e = E.X   // E.X


Vous pouvez utiliser une annotation de type explicite pour l'empêcher de s'étendre:



let a: 'x' = 'x' // 'x'
let b: 3 = 3  // 3
var c: true = true  // true
const d: {x: 3} = {x: 3}  // {x: 3}


Lorsque vous réaffectez un type non étendu avec let ou var, TypeScript l'étend pour vous. Pour éviter cela, ajoutez une annotation de type explicite à la déclaration d'origine:



const a = 'x' // 'x'
let b = a  // string
const c: 'x' = 'x'  // 'x'
let d = c  // 'x'


Les variables initialisées à null ou indéfinies se développent en tout:



let a = null // any
a = 3  // any
a = 'b'  // any


Mais, lorsqu'une variable, initialisée à null ou non définie, quitte la portée dans laquelle elle a été déclarée, TypeScript lui attribue un type spécifique:



function x() {
   let a = null  // any
   a = 3   // any
   a = 'b'   // any
   return a
}
x()   // string


Le



type const Le type const permet d'éviter d'étendre la déclaration de type. Utilisez-le comme une assertion de type (voir la sous-section «Homologations de type» à la page 185):



let a = {x: 3}   // {x: number}
let b: {x: 3}    // {x: 3}
let c = {x: 3} as const   // {readonly x: 3}


const élimine l'expansion de type et marque récursivement ses membres comme en lecture seule, même dans les structures de données profondément imbriquées:



let d = [1, {x: 2}]              // (number | {x: number})[]
let e = [1, {x: 2}] as const    // readonly [1, {readonly x: 2}]


Utilisez comme const lorsque vous souhaitez que TypeScript déduit le type le plus étroit possible.



Vérification des propriétés supplémentaires L'



expansion de type entre également en jeu lorsque TypeScript vérifie si un type d'objet est compatible avec un autre type d'objet.



Les types d'objets sont covariants dans leurs membres (voir la sous-section «Variation de forme et de tableau» à la page 148). Mais, si TypeScript suit cette règle sans vérifications supplémentaires, des problèmes peuvent survenir.



Par exemple, considérez un objet Options que vous pouvez passer à une classe pour le personnaliser:



type Options = {
    baseURL: string
    cacheSize?: number
    tier?: 'prod' | 'dev'
}
class API {
    constructor(private options: Options) {}
}
new API({
     baseURL: 'https://api.mysite.com',
     tier: 'prod'
})


Que se passe-t-il maintenant si vous faites une erreur dans l'option?



new API({
   baseURL: 'https://api.mysite.com',
   tierr: 'prod'         //  TS2345:   '{tierr: string}'
})                      //     'Options'.
                        //     
                       //  ,  'tierr'  
                      //   'Options'.    'tier'?


Il s'agit d'un bogue JavaScript courant, et il est bon que TypeScript vous aide à le détecter. Mais si les types d'objets sont covariants dans leurs membres, comment TypeScript les intercepte-t-il?



En d'autres termes:



  • Nous attendions le type {baseURL: string, cacheSize?: Number, tier?: 'Prod' | 'dev'}.
  • Nous avons passé le type {baseURL: string, tierr: string}.
  • Le type passé est un sous-type du type attendu, mais TypeScript savait signaler une erreur.


En recherchant des propriétés supplémentaires , lorsque vous essayez d'attribuer un nouveau type d'objet littéral T à un autre type, U et T ont des propriétés que U n'a pas, TypeScript signale une erreur.



Le nouveau type d' objet littéral est le type que TypeScript a déduit d'un objet littéral. Si ce littéral d'objet utilise une assertion de type (voir la sous-section «Assertions de type» à la page 195) ou est affecté à une variable, alors le nouveau type est étendu au type d'objet normal et sa nouveauté est perdue.



Essayons de rendre cette définition plus vaste:



type Options = {
     baseURL: string
     cacheSize?: number
     tier?: 'prod' | 'dev'
}
class API {
    constructor(private options: Options) {}
}
new API({ ❶
    baseURL: 'https://api.mysite.com',
    tier: 'prod'
})
new API({ ❷
    baseURL: 'https://api.mysite.com',
    badTier: 'prod' //  TS2345:   '{baseURL:
}) // string; badTier: string}' 
//    'Options'.
new API({ ❸
    baseURL: 'https://api.mysite.com',
    badTier: 'prod'
} as Options)
let badOptions = { ❹
    baseURL: 'https://api.mysite.com',
    badTier: 'prod'
}
new API(badOptions)
let options: Options = { ❺
    baseURL: 'https://api.mysite.com',
    badTier: 'prod' //  TS2322:  '{baseURL: string;
} // badTier: string}'  
// 'Options'.
new API(options)


❶ Instanciez l'API avec baseURL et l'une des deux propriétés facultatives: tier. Tout fonctionne.



❷ Nous écrivons par erreur tier comme badTier. L'objet options que nous transmettons à la nouvelle API est nouveau (son type est déduit, il est incompatible avec la variable, et nous ne saisissons pas d'assertions pour lui), donc lors de la vérification des propriétés inutiles, TypeScript détecte une propriété badTier supplémentaire (qui est définie dans l'objet options, mais pas dans le type Options).



❸ Déclarez que l'objet d'options non valide est de type Options. TypeScript ne le considère plus comme nouveau et conclut de la vérification des propriétés supplémentaires qu'il n'y a pas d'erreurs. La syntaxe as T est décrite dans la section «Assertions de type» à la p. 185.



❹ Affectation de l'objet options à la variable badOptions. TypeScript ne le perçoit plus comme nouveau et, après avoir vérifié les propriétés inutiles, conclut qu'il n'y a pas d'erreurs.



❺ Lorsque nous tapons explicitement des options comme Options, l'objet que nous attribuons aux options est nouveau, donc TypeScript vérifie les propriétés supplémentaires et trouve un bogue. Notez que dans ce cas, la vérification des propriétés supplémentaires n'est pas effectuée lorsque nous transmettons des options à la nouvelle API, mais elle le fait lorsque nous essayons d'attribuer un objet options à la variable d'options.



Vous n'avez pas besoin de mémoriser ces règles. C'est juste une heuristique TypeScript interne pour attraper autant de bogues que possible. Gardez-les à l'esprit si vous vous demandez soudainement comment TypeScript a découvert un bogue que même Ivan - un ancien de votre entreprise et également un censeur de code professionnel - n'a pas remarqué.



Le raffinement



TypeScript effectue une exécution symbolique de l'inférence de type. Le module de vérification de type utilise des instructions de flux de commandes (comme if,?, || et switch) ainsi que des requêtes de type (comme typeof, instanceof et in), qualifiant ainsi les types au fur et à mesure qu'un programmeur lit le code. Cependant, cette fonctionnalité pratique est prise en charge dans très peu de langues.



Imaginez que vous ayez développé une API pour définir des règles CSS dans TypeScript, et que votre collègue souhaite l'utiliser pour définir la largeur d'un élément HTML. Il passe la largeur que vous souhaitez analyser et vérifier plus tard.



Tout d'abord, implémentons une fonction pour analyser une chaîne CSS en valeur et unité:



//       
//  ,      CSS
type Unit = 'cm' | 'px' | '%'
//   
let units: Unit[] = ['cm', 'px', '%']
//   . .   null,    
function parseUnit(value: string): Unit | null {
  for (let i = 0; i < units.length; i++) {
    if (value.endsWith(units[i])) {
       return units[i]
}
}
     return null
}


Nous utilisons ensuite parseUnit pour analyser la largeur fournie par l'utilisateur. width peut être un nombre (éventuellement en pixels), ou une chaîne avec les unités attachées, ou nul, ou non défini.



Dans cet exemple, nous utilisons plusieurs fois la qualification de type:



type Width = {
     unit: Unit,
     value: number
}
function parseWidth(width: number | string | null |
undefined): Width | null {
//  width — null  undefined,  .
if (width == null) { ❶
     return null
}
//  width — number,  .
if (typeof width === 'number') { ❷
    return {unit: 'px', value: width}
}
//      width.
let unit = parseUnit(width)
if (unit) { ❸
return {unit, value: parseFloat(width)}
}
//     null.
return null
}


❶ TypeScript est capable de comprendre que l'égalité lâche de JavaScript par rapport à null renverra true à la fois pour null et pour undefined. Il sait aussi que si la vérification réussit, alors nous ferons un retour, et si nous ne faisons pas de retour, alors la vérification a échoué et à partir de ce moment, le type de largeur est nombre | string (il ne peut plus être nul ou indéfini). On dit que le type a été raffiné à partir du nombre | chaîne | null | indéfini en nombre | chaîne.



❷ La vérification de typeof demande une valeur à l'exécution pour voir son type. TypeScript tire également parti de typeof au moment de la compilation: dans la branche if où le test passe, TypeScript sait que la largeur est un nombre. Sinon (si cette branche renvoie) width devrait être une chaîne - le seul type restant.



❸ Puisque parseUnit peut renvoyer null, nous vérifions cela. TypeScript sait que si l'unité est correcte, elle doit être de type Unité dans la branche if. Sinon, unit n'est pas valide, ce qui signifie que son type est nul (raffiné à partir de Unit | null).



❹ Enfin, nous retournons null. Cela ne peut se produire que si l'utilisateur transmet une chaîne pour la largeur, mais que cette chaîne contient des unités non prises en charge.

J'ai parcouru le train de pensée de TypeScript pour chaque raffinement de type effectué. TypeScript fait un excellent travail en prenant votre raisonnement lorsque vous lisez et écrivez du code et en le cristallisant en vérification de type et ordre d'inférence.



Types de jointures discriminés



Comme nous venons de le découvrir, TypeScript a une bonne compréhension du fonctionnement de JavaScript et est capable de suivre nos qualifications de type comme si nous lisions dans nos pensées.



Disons que nous créons un système d'événements personnalisé pour une application. Nous commençons par définir les types d'événements ainsi que les fonctions qui gèrent l'arrivée de ces événements. Imaginez que UserTextEvent simule un événement clavier (par exemple, l'utilisateur a tapé le texte <input />), et UserMouseEvent simule un événement souris (l'utilisateur a déplacé la souris aux coordonnées [100, 200]):



type UserTextEvent = {value: string}
type UserMouseEvent = {value: [number, number]}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
     if (typeof event.value === 'string') {
         event.value // string
         // ...
         return
   }
         event.value // [number, number]
}


TypeScript sait qu'à l'intérieur du bloc if, event.value doit être une chaîne (grâce à la vérification de typeof), c'est-à-dire que event.value après le bloc if doit être un tuple [number, number] (à cause du retour dans le bloc if).



Où mènera la complication? Ajoutons des clarifications aux types d'événements:



type UserTextEvent = {value: string, target: HTMLInputElement}
type UserMouseEvent = {value: [number, number], target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
    if (typeof event.value === 'string') {
        event.value // string
        event.target // HTMLInputElement | HTMLElement (!!!)
        // ...
        return
   }
  event.value // [number, number]
  event.target // HTMLInputElement | HTMLElement (!!!)
}


Bien que la clarification ait fonctionné pour event.value, ce n'était pas le cas pour event.target. Pourquoi? Lorsque le handle reçoit un paramètre de type UserEvent, cela ne signifie pas que vous devez lui passer UserTextEvent ou UserMouseEvent - en fait, vous pouvez passer un argument de type UserMouseEvent | UserTextEvent. Et comme les membres d'une union peuvent se chevaucher, TypeScript a besoin d'un moyen plus fiable pour savoir quand et quel cas d'union est pertinent.



Vous pouvez le faire en utilisant des types littéraux et une définition de balise pour chaque cas de type union. Belle balise:



  • Dans chaque cas, il est situé au même endroit du type d'union. Implique le même champ d'objet lorsqu'il s'agit de combiner des types d'objets, ou le même index lorsqu'il s'agit de combiner des tuples. Dans la pratique, les syndicats discriminés sont le plus souvent des objets.
  • Typé comme un type littéral (chaîne littérale, numérique, booléenne, etc.). Vous pouvez mélanger et assortir différents types de littéraux, mais il est préférable de s'en tenir à un seul type. Il s'agit généralement d'un type de chaîne littérale.
  • Pas universel. Les balises ne doivent pas recevoir d'arguments de type générique.
  • Mutuellement exclusif (unique dans le type d'union).


Mettons à jour les types d'événements en tenant compte de ce qui précède:



type UserTextEvent = {type: 'TextEvent', value: string,
                                        target: HTMLInputElement}
type UserMouseEvent = {type: 'MouseEvent', value: [number, number],
                                        target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
   if (event.type === 'TextEvent') {
       event.value // string
       event.target // HTMLInputElement
       // ...
       return
   }
  event.value // [number, number]
  event.target // HTMLElement
}


Désormais, lorsque nous affinons l'événement en fonction de la valeur de son champ balisé (event.type), TypeScript sait qu'il devrait y avoir un UserTextEvent dans la branche if, et après la branche if, il devrait avoir un UserMouseEvent.Parce que les balises sont uniques dans chaque type d'union, TypeScript sait qu’ils s’excluent mutuellement.



Utilisez des jointures discriminées lors de l'écriture d'une fonction qui gère divers cas de type de jointure. Par exemple, lorsque vous travaillez avec des actions Flux, des restaurations redux ou useReducer dans React.



Vous pouvez vous familiariser avec le livre plus en détail et le précommander à un prix spécial sur le site de l'éditeur



All Articles