Ça y est: vos collègues ont enfin réussi à vous motiver à utiliser
flow, l'outil de Facebook vous permettant d'ajouter du
typage fort au sein de vos fichiers JavaScript. Seulement voilà, vous avez
utilisé des langages à typage dynamique faible toute votre vie (PHP, JavaScript,
Ruby, Python…), et pour le moment vous vous contentez d'ajouter des annotations
de types Object
, Function
ou encore string
. Frustrés, vous ne pouvez vous
empêcher de crier sur tous les toits que le typage, c'est quand même chiant et
limite une perte de temps puisque flow continue de manquer un tas d'erreurs
potentielles au sein de votre code. Cet article est là pour vous aider à
comprendre de quoi le bouzin est réellement capable, et même si vous n'utilisez
pas flow, il peut servir de chouette introduction un peu poussée à son
utilisation.
Nous utiliserons la dernière version en date à l'heure où j'écris ces lignes, c'est à dire la 0.59. Pour l'ajouter au sein de votre projet, petit rappel:
npm install --save-dev flow-bin
npm install --save-dev flow-remove-types # pour les besoin du tuto - vous pouvez également utiliser babel et le couple babel-plugin-syntax-flow / babel-plugin-transform-flow-strip-types
npx flow init # pour créer le fichier .flowconfig
Pour plus d'infos sur npx, c'est par ici. À noter que c'est livré de base avec npm maintenant.
Flow est un outil intelligent: il est inutile de préciser quel type est utilisé si celui-ci est évident à l'usage.
// @flow <- le pragma nécessaire pour indiquer à flow d'analyser votre fichier. À noter que si vous l'ajoutez sur un projet tout neuf, vous pouvez le configurer pour que celui-ci ne soit pas nécessaire
const quote = "Thirouin rouin rouin rouin";
Math.round(quote); // erreur! le type attendu est un number
const sayHello = name => `Hello ${name}`; // inféré en (name: any) => string
sayHello("Mathieu"); // pas d'erreur
sayHello(42); // pas d'erreur
sayHello(["Mathieu", "zoontek"]); // erreur: ne peut être un array
Attention: En annotant la fonction sayHello
du type Function
, vous perdez
carrément l'inférence. Comme quoi, parfois ne pas préciser le type d'une
variable peut se montrer plus efficace que d'utiliser des types "génériques"
tels que Object
ou Function
(d'ailleurs je vous encourage à ne jamais le
faire).
// @flow
const sayHello: Function = name => `Hello ${name}`; // inféré en (name: any) => any
Si vous désirez interpoler un string, la bonne façon de faire est bien sûr la suivante :
// @flow
const sayHello = (name: string) => `Hello ${name}`; // (name: string) => string
sayHello("Mathieu"); // pas d'erreur
sayHello(42); // pas d'erreur
Le nombre de types primitifs existants en JavaScript est assez restreint. Vous
avez le nombre (number
), la chaine de caractères (string
), les booléens
(boolean
), et bien évidemment les valeurs nulles (null
) et inexistantes
(void
). À noter que ES2015 a également apporté (Symbol
), mais que ce dernier
n'est pas encore supporté par flow.
// @flow
const a: number = 42;
const b: string = "Je ne suis pas Coluche, certes";
const c: boolean = true;
const d: null = null;
const e: void = undefined;
Afin de vous montrer plus précis encore, vous pouvez également utiliser des
valeurs litérales comme type. Après tout string
, ça peut être tout et
n'importe quoi. Si cela peut vous sembler stupide dans un premier temps, ceux-ci
sont extrêmement utiles lorsque le système de typage est suffisamment
perfectionné, commme vous le verrez par la suite.
// @flow
const foo: "x" = "x"; // pas d'erreur
const bar: 2 = 3; // erreur! n'est pas égal à 2
any
, mixed
et *
Comme expliqué au dessus, utiliser any
revient à dire à flow qu'une variable
peut être de n'importe quel type et cela est bien sûr extrêmement dangereux.
Heureusement, il existe 2 alternatives plus sûres à connaître :
mixed
qui dit que peu importe le type de la variable passée à une fonction,
l'appel de celle-ci doit pouvoir se faire correctement: vous ne pourrez donc
pas y appeler des méthodes spécifiques à un type de variable en particulier.*
qui laisse travailler l'inférence de type de flow.// @flow
function foo(arg: mixed) {
console.log(arg);
} // pas d'erreur: peu importe le type réel du paramètre arg, l'appel se fera correctement
function bar(arg: mixed) {
console.log(arg.toUpperCase());
} // erreur! arg pourrait ne pas être une string
const baz: Array<*> = [1, 2, 3]; // inféré en Array<number>
baz.push("Hello"); // "foo" est maintenant inféré en Array<number | string> (tableau de string ou de numbers)
Si vous avez déjà utilisé un langage qui essaye d'éviter
l'erreur à un milliard de dollars,
vous connaissez sûrement les types Option
/ Maybe
. Ils représentent la
possible absence d'une valeur et sont ici symbolisés à l'aide d'un point
d'interrogation.
// @flow
let foo: ?string; // peut-être une string, null ou undefined
foo = "foo"; // pas d'erreur
foo = undefined; // pas d'erreur
foo = null; // pas d'erreur
foo = 3; // erreur - number n'est ni une string, ni null, ni undefined
type Bar = {
baz?: string, // baz sera forcément une string si elle est présente
};
function wrongToUpperCase(str: ?string) {
return str.toUpperCase(); // erreur, str est possiblement nul, vous devez traiter ce cas
}
function correctToUpperCase(str: ?string) {
if (typeof str === "string") {
return str.toUpperCase(); // pas d'erreur
}
}
Vous souvenez-vous de la syntaxe des tableaux (Array<any>
) croisée plus tôt?
Il s'agit de ce qu'on appelle un type générique : un type construit depuis un
autre type. Ainsi, si Array<any>
symbolise un tableau de tout et n'importe
quoi, Array<number>
symbolisera un tableau de numbers, etc.
// @flow
// on crée un type Classement qui prend en "paramètre" un type générique qu'on nomme "T"
type Classement<T> = {
first: T,
second: T,
third: T,
};
const numberClassement: Classement<number> = {
first: 42,
second: 7,
third: 1,
};
Voici quelques exemples de types génériques disponibles out-of-the-box :
// @flow
const foo: Array<number> = [];
const bar: Map<number, string> = new Map();
bar.set(1, "one");
const baz: Set<number> = new Set();
baz.add(1);
const p: Promise<number> = Promise.resolve(42);
Avez vous remarqué que pour le moment, j'ai tenté de ne pas utiliser d'objets au sein de mes exemples (oui, c'était chiant)? C'est tout simplement car les possibilités de manipulation des types de ceux-ci sont très nombreuses. Je vous propose un exemple fleuve histoire d'y voir plus clair.
Un type objet se définit de la sorte :
// @flow
type User = {
name: string,
pseudo: string,
};
const user: User = {
name: "Mathieu",
pseudo: "zoontek",
};
// Ce qui n'exclut pas de faire
const newUser: User = {
...user,
age: 26,
}; // pas d'erreur
Pour sceller les propriétés de notre objet, on utilise la notation {||}
:
// @flow
type User = {|
name: string,
pseudo: string,
|};
const user: User = {
name: "Mathieu",
pseudo: "zoontek",
};
const newUser: User = {
...user,
age: 26,
}; // erreur! la propriété age n'existe pas
Pour indiquer qu'une propriété de notre objet est immutable, on utilise la
notation +
. Un must-have pour vos reducers Redux.
// @flow
type User = {
+name: string,
pseudo: string,
};
const user: User = {
name: "Mathieu",
pseudo: "zoontek",
};
user.name = "Jean-Michel"; // erreur! la valeur de name ne peut-être modifiée
const newUser: User = {
...user,
name: "Jean-Michel",
}; // pas d'erreur
Le spreading c'est vraiment cool! Ça existe aussi pour les types?
Yep.
// @flow
type Foo = {| a: number, b: string |};
type Bar = {| a: string |};
type Baz = {| ...Foo, ...Bar |}; // {| a: string, b: string |}
Un dernier petit trick sur les objets pour la route? Vous pouvez les utiliser comme maps :
// @flow
const foo: { [key: string]: number } = {
bar: 42,
baz: "whatever", // erreur! les valeurs des propriétés de cet objet doivent être de type number
};
Si vous avez remarqué la notation avec |
plus tôt, vous vous posez sûrement la
question de ce que ça représente. Il s'agit d'une union de types : la variable
aura une valeur à plusieurs types possibles. Petit conseil : utilisez toujours
une union de types litéraux à la place du simple string
lorsque vous
connaissez à l'avance les possibles valeurs de celui-ci.
// @flow
function sayHelloOrRound(arg: string | number) {
// arg peut être une string ou un number
return typeof arg === "string" ? `Hello ${arg}` : Math.round(arg);
} // pas d'erreur
type Color = "red" | "green" | "blue";
function toHexadecimal(color: Color) {
switch (color) {
case "red":
return "#FF0000";
case "green":
return "#00FF00";
case "blue":
return "#0000FF";
}
}
toHexadecimal("red"); // pas d'erreur
toHexadecimal("green"); // pas d'erreur
toHexadecimal("blue"); // pas d'erreur
toHexadecimal("pink"); // erreur! "pink" n'est pas une valeur possible
toHexadecimal("dog"); // erreur! "dog" n'est pas une valeur possible
Parfois, une union de types se montre également bien plus efficace pour modéliser ce que vous souhaitez, à contrario d'un tas de maybe types.
// @flow
type ApiResponseBadlyTyped = {|
success: boolean,
value?: string, // value peut être présent
error?: Error, // error peut être présent
|};
const foo: ApiResponseBadlyTyped = { success: true }; // pas d'erreur alors que j'attends une value
const bar: ApiResponseBadlyTyped = { success: true, error: new Error("oups!") }; // pas d'erreur non plus
type ApiResponseCorrectlyTyped =
| {| success: true, value: string |}
| {| success: false, error: Error |};
const foo: ApiResponseCorrectlyTyped = { success: true }; // erreur! il manque value
const bar: ApiResponseCorrectlyTyped = {
success: true,
error: new Error("oups!"),
}; // erreur! si success est à true, on ne doit pas trouver d'error dans notre objet
const baz: ApiResponseCorrectlyTyped = {
success: false,
error: new Error("oups!"),
}; // pas d'erreur
Si les unions de types sont le OU logique du système de typage, les
intersections de types en sont le ET. On les symbolise à l'aide d'un &
.
// @flow
type Foo = { a: number };
type Bar = { b: string };
type Baz = { c: boolean };
const test: Foo & Bar & Baz = {
a: 42,
b: "whatever",
c: true,
}; // pas d'erreur
let impossible: number & string; // sera forcément impossible à initialiser : une valeur ne pourra jamais être un number ET une string
C'est tout pour le moment! Vous vous doutez que l'on égratigne à peine la surface de ce qui nous est offert par flow et les systèmes de typage fort en général. Si vous êtes vraiment impatients de découvrir la suite, je vous renvoie vers la documentation de flow, très bien foutue. Pour les autres, on devrait sortir très prochainement un article sur le même sujet (on y parlera classes, interfaces et peut être même types opaques si vous êtes sages).
Stay tuned! La bise.