Emulation de typage nominal en TypeScript
À la réflexion, ce titre sonne un peu "Les chevaliers de l'an mille au lac de Paladru"… Vous avez la ref ? Laissez-moi vous guider.
On parle de typage nominal lorsqu'un type est défini par son nom. Java propose un typage nominal. Si 2 types ont des noms différents, par exemple Numerator
et Denominator
, le compilateur les considérera comme différents quand bien même ces types seraient identiques dans les faits (même comportement).
Cela s'oppose au typage structurel, dans lequel un type est défini par sa structure. TypeScript propose un typage structurel. Par exemple, toujours au sujet des fractions, les types Numerator
et Denominator
définis ci-dessous sont entièrement substituables du point de vue du compilateur :
type Numerator = {
value: number;
};
type Denominator = {
value: number;
};
On parle (très sérieusement) de duck typing, parce que si ça vole et que ça fait "coin coin" comme un canard, c'est que c'est un canard 🦆
Le choix d'un typage structurel par les créateurs de TypeScript présente de nombreux avantages, qui sont largement documentés sur le Web. Malheureusement, ce choix s'avère limitant pour qui veut faire de la programmation défensive, qui vise à "assurer le fonctionnement continu d'un logiciel dans des circonstances imprévues".
Cette approche suppose un état d'esprit (à juste titre) paranoïaque, qui nous interdit de faire confiance à qui que ce soit, développeur ou utilisateur : que ce soit intentionnel ou non, des données vous parviendront qui mettront le programme en échec. Exemple : la fameuse division par 0.
L'idée est donc d'effectuer les validations nécessaires le plus tôt possible et d'encapsuler la donnée validée dans une abstraction que l'on pourra manipuler en toute sécurité dans la suite du programme. D'où la recommandation d'encapsuler les primitives du langage (string
, number
etc.) et de faire émerger les abstractions métier correspondantes (Domain-driven design mon Amour 💜).
Ainsi, nous ne pouvons pas nous contenter des types Numerator
et Denominator
précédemment définis. Ces types étant identiques du point de vue du compilateur, ils peuvent être intervertis par erreur du développeur ou de l'utilisateur. Tôt ou tard, un dénominateur sera pris pour un numérateur (problématique en soi), ou réciproquement (encore plus problématique car cela pourrait conduire à une division par 0).
Autrement dit, nous voudrions les garanties du typage nominal, tandis que nous travaillons avec un typage structurel.
Que faire ?
Eh bien nous pouvons toujours émuler un typage nominal. Pour cela, nous allons utiliser :
-
Un symbole (
DENOMINATOR_SYMBOL
), que nous aurons soin de ne pas exporter ; -
Un constructeur (
createDenominator
), fonction qui accède au symbole et l'utilise comme clef dans l'objet qu'elle crée. Ce constructeur effectue les contrôles sans lesquels il serait abusif de parler de dénominateur.
Ainsi :
-
Les types
Numerator
etDenominator
ne sont plus intervertibles ; -
Créer un objet
Denominator
n'est possible qu'en passant par le constructeur, interdisant de ce fait des dénominateurs nuls.
Elle est pas belle, la vie ? 🏝️🍸
Le code complet est disponible sur GitHub : https://github.com/mathieueveillard/nominal-typing-emulation