En JavaScript, il est courant d'avoir une fonction qui peut être appelée de plusieurs façons - par exemple avec une poignée d'arguments positionnels ou un seul objet d'options ou une combinaison de les deux.
J'ai essayé de savoir comment annoter cela.
J'ai essayé d'annoter les arguments de repos comme une union de divers tuples possibles:
type Arguments =
| [string]
| [number]
| [string, number]
;
const foo = (...args: Arguments) => {
let name: string;
let age: number;
// unpack args...
if (args.length > 1) {
name = args[0];
age = args[1];
} else if (typeof args[0] === 'string') {
name = args[0];
age = 0;
} else {
name = 'someone';
age = args[1];
}
console.log(`${name} is ${age}`);
};
// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);
L'extrait ci-dessus est artificiel; Je pourrais probablement utiliser (...args: Array<string | number>)
dans cet exemple, mais pour des signatures plus complexes (impliquant par exemple un objet d'options typé qui peut être seul ou avec des arguments antérieurs), il serait utile de pouvoir définir un ensemble précis et fini de signatures d'appel possibles.
Mais ce qui précède ne vérifie pas le type. Vous pouvez voir un tas d'erreurs déroutantes dans tryflow .
J'ai également essayé de taper la fonction elle-même comme une union de définitions de fonctions entières distinctes, mais cela n'a pas fonctionné soit:
type FooFunction =
| (string) => void
| (number) => void
| (string, number) => void
;
const foo: FooFunction = (...args) => {
let name: string;
let age: number;
// unpack args...
if (args.length > 1) {
name = args[0];
age = args[1];
} else if (typeof args[0] === 'string') {
name = args[0];
age = 0;
} else {
name = 'someone';
age = args[1];
}
console.log(`${name} is ${age}`);
};
// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);
Comment dois-je aborder les fonctions d'annotation de type avec plusieurs signatures d'appel possibles? (Ou les signatures multiples sont-elles considérées comme un anti-modèle dans Flow, et je ne devrais pas le faire du tout - dans ce cas, quelle est l'approche recommandée pour interagir avec les bibliothèques tierces qui le font?)
Les erreurs que vous voyez sont une combinaison d'un bogue dans votre code et d'un bogue dans Flow.
Commençons par corriger votre bug. Dans la troisième instruction else, vous affectez la mauvaise valeur à
} else {
name = 'someone';
age = args[1]; // <-- Should be index 0
}
Changer l'accès à la baie pour être l'index correct supprime deux erreurs. Je pense que nous pouvons tous deux convenir que c'est exactement à cela que sert Flow, en trouvant des erreurs dans votre code.
Afin de trouver la cause première du problème, nous pouvons être plus explicites dans la zone où se trouvent les erreurs afin de voir plus facilement quel est le problème:
if (args.length > 1) {
const args_Tuple: [string, number] = args;
name = args_Tuple[0];
age = args_Tuple[1];
} else if (typeof args[0] === 'string') {
C'est effectivement la même chose qu'avant, mais parce que nous sommes très clairs sur ce que args[0]
Et args[1]
Devraient être à ce stade. Cela nous laisse avec une seule erreur.
L'erreur restante est un bogue dans Flow: https://github.com/facebook/flow/issues/3564
bogue: le type de tuple n'interagit pas avec les assertions de longueur (.length> = 2 et [] | [nombre] | [numéro, numéro] type)
Flow n'est pas très efficace pour gérer des variadics de types différents, comme dans ce cas. Les variétés sont plus pour des choses comme function sum(...args: Array<number>)
où tous les types sont les mêmes et il n'y a pas d'arité maximale.
Au lieu de cela, vous devriez être plus explicite avec vos arguments, comme ceci:
const foo = (name: string | number, age?: number) => {
let real_name: string = 'someone';
let real_age: number = 0;
// unpack args...
if (typeof name === 'number') {
real_age = name;
} else {
real_name = name;
real_age = age || 0;
}
console.log(`${real_name} is ${real_age}`);
};
// any of these call signatures should be OK:
foo('fred');
foo('fred', 30);
foo(30);
Cela ne provoque aucune erreur et je pense que c'est aussi plus facile à lire pour les développeurs.
Dans une autre réponse, Pavlo a fourni une autre solution que j'aime plus que la mienne.
type Foo =
& ((string | number) => void)
& ((string, number) => void)
const foo: Foo = (name, age) => {...};
Il résout les mêmes problèmes d'une manière beaucoup plus propre, vous offrant ainsi beaucoup plus de flexibilité. En créant une intersection de plusieurs types de fonctions, vous décrivez chaque façon différente d'appeler votre fonction, permettant à Flow de l'essayer en fonction de la façon dont la fonction est appelée.
Vous pouvez définir plusieurs signatures de fonction en les joignant à &
:
type Foo =
& ((string | number) => void)
& ((string, number) => void)
Parmi les trois entraînements possibles que vous avez donnés, j'ai trouvé comment le faire fonctionner en utilisant un seul objet d'options, mais comme vous avez besoin d'au moins un objet à définir, vous devez définir chaque possibilité.
Comme ça:
type Arguments =
{|
+name?: string,
+age?: number
|} |
{|
+name: string,
+age?: number
|} |
{|
+name?: string,
+age: number
|};
const foo = (args: Arguments) => {
let name: string = args.name ? args.name : 'someone';
let age: number = typeof args.age === 'number' && !isNaN(args.age) ? args.age : 0;
console.log(`${name} is ${age}`);
}
// any of these call signatures are OK:
foo({ name: 'fred' });
foo({ name: 'fred', age: 30 });
foo({ age: 30 });
// fails
foo({});