Je vous suggère fortement de remanier votre structure de données dans un format qui fonctionne bien avec le système de types TypeScript et le moteur d'exécution JavaScript. Voici la forme que j'attendrais SelectOption
à avoir :
interface SelectOption {
idKey: string,
idValue: string,
elKey: string,
elValue: Node
}
Vous savez maintenant exactement quelle sera la clé de chaque valeur. Si vous devez traiter l'une de ces valeurs, vous pouvez le faire facilement :
function processSelectOption(selectOption: SelectOption) {
console.log("id at key " + selectOption.idKey +
" with string value \"" + selectOption.idValue + "\"");
console.log("el at key " + selectOption.elKey +
" with Node value " + JSON.stringify(selectOption.elValue));
}
processSelectOption({ idKey: "id", idValue: "xyz", elKey: "el", elValue: node });
// id at key id with string value "xyz"
// el at key el with Node value ["a","b","c"]
Comparez cela à ce que vous devriez faire avec votre structure de données actuelle, où tout ce que vous savez est qu'une clé a une valeur de type string
et qu'une autre clé a une valeur de type Node
:
function processSelectOption(selectOption: any) {
// for now, let's use any ---------------> ^^^
function isString(x: any): x is string { return typeof x === "string" }
function isNode(x: any): x is Node {
return ["string", "function"].includes(typeof x) ||
(Array.isArray(x) && x.every(w => ["string", "function"].includes(typeof w)));
}
function findSelectOptionData(selectOption: any) {
for (const idKey in selectOption) {
for (const elKey in selectOption) {
if (elKey === idKey) continue;
const idValue = selectOption[idKey];
if (!isString(idValue)) continue;
const elValue = selectOption[elKey];
if (!isNode(elValue)) continue;
return { idKey, idValue, elKey, elValue };
}
}
return;
}
const selectOptionData = findSelectOptionData(selectOption);
if (!selectOptionData) throw new Error("COULDN'T FIND IT");
// now selectOptionData is the same as the SelectOption I proposed above
console.log("id at key " + selectOptionData.idKey +
" with string value \"" + selectOptionData.idValue + "\"");
console.log("el at key " + selectOptionData.elKey +
" with Node value " + JSON.stringify(selectOptionData.elValue));
}
Voyez comment nous devons écrire des tests d'exécution pour l'identité string
y Node
car elles peuvent se trouver sur n'importe quelle propriété. Nous devons également itérer sur toutes les paires de propriétés de selectOption
pour en trouver un qui soit un string
et un autre qui est un Node
. (Ainsi, si selectOption
a des clés, alors vous itérez sur O(²) éléments pour identifier les propriétés). Et une fois que vous avez fait tout cela, vous avez finalement obtenu les quatre mêmes éléments d'information à partir de l'élément SelectOption
que j'avais proposée à l'origine :
processSelectOption({ id: "xyz", el: node });
// id at key id with string value "xyz"
// el at key el with Node value ["a","b","c"]
Et même une fois que vous l'aurez fait, vous pourrez obtenir des résultats surprenants :
processSelectOption({ el: "node", id: "str" });
// id at key el with string value "node"
// el at key id with Node value "str"
Depuis string
s'étend Node
Il n'y a aucun moyen de regarder une paire de propriétés de chaînes et de déterminer laquelle est "supposée" être l'id et laquelle est supposée être l'élément. Il faut donc effectuer un grand nombre de traitements avant d'arriver à un point où il y a une ambiguïté sur ce qui est censé faire quoi. Cette ambiguïté s'aggrave avec l'augmentation du nombre de propriétés :
processSelectOption({ foo: 123, bar: "baz", id: "str", el: node });
// id at key bar with string value "baz"
// el at key id with Node value "str"
Sans connaître votre cas d'utilisation complet, je ne peux pas en être certain, mais de l'extérieur, il semble qu'une telle structure de données ne soit pas envisageable. Et cela ne concerne que le temps d'exécution.
Dans le système des types, la même ambiguïté bizarre existe. Le langage n'est pas vraiment conçu pour compter le nombre de propriétés existantes et s'il existe une propriété de type A
et une propriété distincte de type B
. Il n'y a certainement pas de type spécifique qui rende compte de cette notion. interface SelectOption {/*...*/}
o type SelectOption = ...
est perdue.
Vous peut l'exprimer comme une sorte de contrainte sur un type. Si vous avez un type candidat T
vous pouvez écrire un générique type appelé AsValidSelectOption<T>
qui prend en compte le type de candidat T
et produit un SelectOption
qui est "proche" de T
dans un sens mal défini. Si T
est valide, alors nous voulons être sûrs que T extends AsValidSelectOption<T>
. Si T
n'est pas valide, alors nous voulons AsValidSelectOption<T>
d'être quelque chose de valable qui est "proche" de T
afin que les messages d'erreur mentionnent ce qui ne va pas d'une manière conviviale.
Voyons cela maintenant :
Commençons par écrire AtLeastTwoElements<K>
qui prend un syndicat de types de clés L
et évalue à el unknown
type supérieur s'il y a au moins deux éléments dans le K
l'union, ou bien el never
type de fond s'il y a moins de deux éléments :
type AtLeastTwoElements<K extends PropertyKey> =
{ [P in K]: { [Q in Exclude<K, P>]: unknown }[Exclude<K, P>] }[K];
Il s'agit d'un type cartographié alors que dans les types internes, nous utilisons el Exclude
type d'utilité pour retirer successivement des clés de K
. Si nous pouvons le faire une fois et qu'il reste des clés, c'est qu'il y a au moins deux clés dans cette union. AtLeastTwoElements<"a" | "b">
s'évalue à {a: {b: unknown}["b"], b: {a: unknown}["a"]}["a" | "b"]
qui est {a: unknown, b: unknown}["a" | "b"]
qui est unknown
. Mais AtLeastTwoElements<"a">
es {a: {}[never], b: {}[never]}["a" | "b"]
qui est {a: never, b: never}["a" | "b"]
qui est never
. Et AtLeastTwoElements<never>
es {}[never]
qui est never
.
On écrit alors ValidSelectOptionsWithKeys<K>
qui prend une union de types de clés K
et produit une grande union de tous les SelectOption
à l'aide de ces clés :
type ValidSelectOptionsWithKeys<K extends PropertyKey> = { [P in K]:
Recordlt;P, Node> & { [Q in Exclude<K, P>]: Record<Q, string> }[Exclude<K, P>]
}[K] extends infer O ? O extends any ? { [P in keyof O]: O[P] } : never : never;
Cela peut sembler compliqué, mais c'est en fait assez similaire à la façon dont le findSelectOptionData()
fonctionne ci-dessus, en itérant sur chaque clé et en la traitant comme une Node
puis en itérant sur toutes les clés restantes et en les traitant comme des string
. S'il y a exactement deux clés "a" | "b"
alors elle est évaluée à quelque chose comme {a: {a: Node}&{b: {b: string}}["b"], b: {b: Node}&{a: {a: string}["a"]}}["a" | "b"]
qui est {a: {a: Node, b: string}, b: {b: Node, a: string}}["a" | "b"]
qui est {a: Node, b: string} | {a: string, b: Node}
. Le nombre de possibilités augmente avec le nombre d'entrées dans K
. Pour trois touches, vous avez quelque chose comme {a: Node, b: string} | {a: Node, c: string} | {b: Node, a: string} | {b: Node, c: string} | {c: Node, a: string} | {c: Node, b: string}
. Ainsi, si K
a des éléments, alors le type produit est une union de O(²) éléments.
Enfin, nous construisons AsValidSelectOption<T>
:
type AsValidSelectOption<T extends object> =
unknown extends AtLeastTwoElements<keyof T> ? ValidSelectOptionsWithKeys<keyof T> :
T & (
"anotherProp" extends keyof T ? { someOtherProp: Node | string } :
{ anotherProp: Node | string }
);
Si T
a au moins deux éléments, alors nous évaluons ValidSelectOptionsWithKeys<keyof T>
qui T
a intérêt à être assignable pour être valide. Si elle est valide, il est préférable qu'elle puisse être cédée. T
a moins de deux éléments, alors nous évaluons T & {anotherProp: Node | string}
qui T
n'arrivera presque certainement pas à s'étendre, et le message d'erreur indiquera que anotherProp
est manquante. Oh, à moins que vous n'ayez donné un nom à votre clé unique anotherProp
puis nous nous plaignons de someOtherProp
. C'est probablement très peu probable, mais au moins nous avons couvert les bases.
Afin de vérifier si une valeur proposée de type T
s'étend AsValidSelectOption<T>
nous avons besoin d'une fonction d'aide générique à laquelle la transmettre, puisque seules les fonctions génériques déduiront T
pour nous plutôt que de nous obliger à le spécifier manuellement. Voici la fonction asSelectOption
:
const asSelectOption =
<T extends object>(
opt: T extends AsValidSelectOption<T> ? T : AsValidSelectOption<T>
) => opt as T;
Idéalement, j'aimerais écrire <T extends AsValidSelectOption<T>>(opt: T) => opt
mais il s'agit d'une contrainte circulaire. Au lieu de cela, nous contraignons seulement T
a object
mais ont ensuite opt
être du type conditionnel T extends AsValidSelectOption<T> ? T : AsValidSelectOption<T>
. Le compilateur aura donc tendance à choisir T
pour être le type de opt
et le tester. Il s'agit d'une astuce d'inférence.
Il s'agissait donc d'un effort assez fou pour espérer capturer le concept de "une propriété de type string
et une propriété distincte de type Node
". Voyons au moins si cela fonctionne :
declare const node: Node;
const okay0 = asSelectOption({ a: "", b: node });
const okay1 = asSelectOption({ x: node, y: "" });
const okay2 = asSelectOption({ g: "", h: "" });
const okay3 = asSelectOption({ a: "", b: node, c: 123 })
const bad0 = asSelectOption({ a: "", b: 1 }); // number is not Node
const bad1 = asSelectOption({ a: node, b: node }); // error!
// Argument of type '{ a: Node; b: Node; }' is not assignable to
// parameter of type '{ a: Node; b: string; } | { b: Node; a: string; }'
const bad2 = asSelectOption({}) // Property 'anotherProp' is missing
const bad3 = asSelectOption({ a: "" }) // Property 'anotherProp' is missing
const bad4 = asSelectOption({ anotherProp: "" }) // Property 'someOtherProp' is missing
C'est au moins une bonne chose. Les okay*
se compilent sans erreur, car chaque objet est conforme à votre contrainte. Les bad*
Les lignes comportent des erreurs pour une raison ou une autre. Hourra, j'imagine ! Mais à quel prix ?
Et voilà. Si vous vous lancez dans un grand nombre de sauts fous à la fois au moment de la compilation et au moment de l'exécution, vous vous retrouvez avec une implémentation ambiguë, fragile et confuse qui applique votre contrainte et gère (vérifie les notes) quatre valeurs. Si vous remaniez votre structure de données en
interface SelectOption {
idKey: string,
idValue: string,
elKey: string,
elValue: Node
}
vous disposez alors d'une tâche simple, tant à la compilation qu'à l'exécution, où les quatre éléments d'information pertinents se trouvent toujours à des endroits statiquement connus et où les implémentations sont robustes. Peut-être que votre cas d'utilisation rend le saut d'obstacles plus souhaitable que le remaniement, mais encore une fois, vu de l'extérieur, je me méfierais beaucoup d'un projet avec quelque chose comme asSelectOption()
en elle.
Lien vers le code de l'aire de jeux