Vous avez digéré le premier article, peut-être même débuté avec flow depuis, mais vous mourez d'envie de découvrir ce que vous pouvez faire de plus avec votre nouvel outil préféré ? Tant mieux, puisque nous sommes là pour parler d'une fonctionnalité assez avancée, apparue avec la version 0.51 et, à l'heure où j'écris ces lignes, indisponible dans TypeScript: les types opaques.
Si vous n'avez pas installé flow au sein de votre projet, je vous renvoie au premier article qui vous expliquera très bien comment faire.
Si on parle de types opaques, c'est bien parce que les types transparents existent. D'ailleurs par défaut, tout alias de type défini l'est. Mais que sont-ils ? Simple: ce sont des types compatibles entre eux et donc interchangeables.
// @flow
type FirstName = string;
type LastName = string;
function yellFirstName(firstName: FirstName) {
console.log(firstName.toUpperCase());
}
const name: LastName = "Acthernoene";
yellFirstName(name);
Dans cet exemple, les types FirstName
et LastName
étant tout deux des alias
de string
, on peut sans problème utiliser la variable name
de type
LastName
là où la function yellFirstName
attend un paramètre de type
FirstName
. Pas cool.
Nous allons donc modifier l'exemple précédent et…
// @flow
opaque type FirstName = string;
opaque type LastName = string;
function yellFirstName(firstName: FirstName) {
console.log(firstName.toUpperCase());
}
const name: LastName = "Acthernoene";
yellFirstName(name);
…toujours pas d'erreur ? Du calme, c'est normal. Les types FirstName
et
LastName
étant accessibles car dans le même module JS, flow sait que ceux-ci
sont des alias de type string
. Modifions l'exemple et créons un deuxième
module.
// @flow
// module.js
opaque type FirstName = string;
opaque type LastName = string;
type User = {
firstName: FirstName,
lastName: LastName,
};
export function createUser(firstName: string, lastName: string): User {
return { firstName, lastName };
}
export function yellFirstName(firstName: FirstName) {
console.log(firstName.toUpperCase());
}
// @flow
// usage.js
import { createUser, yellFirstName } from "./module";
yellFirstName("Mathieu"); // ⚠️ Erreur: "string. Ce type est incompatible avec le type de paramètre attendu FirstName"
const user = createUser("Acthernoene", "Mathieu");
yellFirstName(user.firstName); // Pas d'erreur
On voit ici qu'il n'est en fait possible d'utiliser le type FirstName
qu'après
son assignation au sein du module où celui-ci est défini. Un autre exemple: si
l'on venait à rendre le type User
opaque, alors nous ne pourrions pas accéder
à user.firstName
, la propriété étant inaccessible autrement que par une
fonction qui manipulera le type User
.
// @flow
// module.js
opaque type FirstName = string;
opaque type LastName = string;
opaque type User = {
firstName: FirstName,
lastName: LastName,
};
export function createUser(firstName: string, lastName: string): User {
return { firstName, lastName };
}
export function yellFirstName(firstName: FirstName) {
console.log(firstName.toUpperCase());
}
export function yellUserFirstName(user: User) {
console.log(user.firstName.toUpperCase());
}
// @flow
// usage.js
import { createUser, yellFirstName, yellUserFirstName } from "./module";
const user = createUser("Acthernoene", "Mathieu");
yellFirstName(user.firstName); // ⚠️ Erreur: "Propriété `firstName`. La propriété n'est pas accessible sur le type User"
yellUserFirstName(user); // Pas d'erreur
Bien sûr que si ! Nous pouvons utiliser les types opaques pour implémenter un
type UUID
très basique:
// @flow
// uuid.js
// @flow
import uuidv4 from "uuid/v4"; // On installe 2 packages
import validator from "validator";
export opaque type UUID = string;
export function create(): UUID {
return uuidv4();
}
export function check(uuid: UUID): boolean {
return validator.isUUID(uuid);
}
// @flow
// usage.js
import * as UUID from "./uuid";
UUID.check("a duck"); // ⚠️ Erreur: "string. Ce type est incompatible avec le type de paramètre attendu UUID"
UUID.check("df7cca36-3d7a-40f4-8f06-ae03cc22f045"); // ⚠️ Erreur: "string. Ce type est incompatible avec le type de paramètre attendu UUID"
const uuid = UUID.create();
UUID.check(uuid); // Pas d'erreur
uuid.toUpperCase(); // ⚠️ Erreur: "Appel de méthode `toUpperCase`. La méthode ne peut pas être appelée sur un type UUID"
Ne vous inquiétez pas, les développeurs à l'origine de flow ont tout prévu et
pour remédier à cela il existe ce qu'on appelle le sous-typage (ou SubTyping
dans la langue de Shakespeare). Ainsi, nous allons dire à flow que chaque UUID
est un string
mais que la réciproque ce sera pas vraie pour autant.
Reprenons l'exemple précédent.
// @flow
import uuidv4 from "uuid/v4";
import validator from "validator";
export opaque type UUID: string = string; // Seul cette ligne change, on y ajoute un sous-type string
export function create(): UUID {
return uuidv4();
}
export function check(uuid: UUID): boolean {
return validator.isUUID(uuid);
}
// @flow
// usage.js
import * as UUID from "./uuid";
const uuid = UUID.create();
UUID.check(uuid); // Pas d'erreur
uuid.toUpperCase(); // Pas d'erreur: même si uuid est un UUID, il est utilisable comme un type string
Comme on a pu le voir, les types opaques élèvent clairement le niveau du type-game dans le milieu du JavaScript. Forcément utiles pour imposer l'utilisation de fonctions pour créer / manipuler certaines données, ils s'avéreront assez vite indispensables pour structurer davantage votre JS et améliorer sa maintenabilité.
Je vous encourage d'ailleurs à vous en servir quasiment partout (ça sera assez chiant au début, mais vous me remercierez plus tard) !
La bise, et rendez-vous pour une troisième partie 😘