Comment pouvons-nous changer le type d'une propriété en fonction de la valeur d'une autre propriété ? Par exemple, retourner un seul élément par défaut, et un tableau d'éléments lorsqu'une propriété multiple est définie.
Pour démontrer ce que je veux dire, regardez l'enregistrement ci-dessous. Ici, vous pouvez observer comment le type change en fonction de l'attribut multiple.

Il y a des cas où vous voudriez réutiliser votre composant à la fois pour des valeurs multiples et simples. Cependant, vous ne voulez pas introduire une autre propriété ou événement emit.
Par exemple, avec un composant Select/MultiSelect, vous pouvez le rendre plus intelligent en lui permettant de retourner un tableau d'éléments lorsque multiple est spécifié et un seul élément lorsqu'il ne l'est pas.
Lisez la suite pour voir comment j'ai réussi à accomplir cela en exploitant les génériques dans Vue 3.3. Les génériques m'ont permis de changer dynamiquement le type d'une propriété en fonction de la valeur d'une autre propriété.
Pourquoi utiliser les génériques dans Vue 3.3 ?
Les génériques dans Vue 3.3 permettent la création de composants flexibles et réutilisables qui peuvent s'adapter à différents scénarios. En utilisant des génériques, vous pouvez modifier conditionnellement le type d'une propriété en fonction des paramètres fournis, réduisant le besoin de code redondant ou de props supplémentaires. Par exemple, pensez à un composant qui peut gérer à la fois des valeurs simples et multiples en fonction d'un indicateur—ce type de flexibilité rend les composants beaucoup plus polyvalents et puissants.
Comprendre le concept
TypeScript peut automatiquement inférer un type générique à partir d'un paramètre dans une fonction générique. Ce type peut ensuite être utilisé pour déterminer le type d'un autre paramètre ou même le type de retour. En combinant cela avec le typage conditionnel, vous pouvez créer une fonction générique qui retourne une valeur différente en fonction du paramètre fourni.
Les génériques dans les composants fonctionnent de manière similaire à ceux des fonctions régulières. Pour mieux comprendre cela, travaillons d'abord sur un exemple utilisant du TypeScript pur avant de l'intégrer dans notre composant Vue.
Exemple TypeScript
// Nous inférons le type de retour du type d'argument.
function discovery<T>(arg: T): T {
return arg;
}
// Chaîne en entrée, chaîne en sortie
const result = discovery('Hello, World!' as string);
// ^ const result: string
Maintenant, nous pouvons restreindre les possibilités de ce type générique en utilisant extends, ce qui nous permet de créer un nouveau type conditionnel basé sur des options spécifiées.
Dans l'exemple suivant, je limite les possibilités à juste true et false. En conséquence, il retourne un tableau de valeurs lorsque true et une seule valeur lorsque false.
function discovery<T extends boolean>(multiple: T) {
type ConditionalType = T extends false ? string : string[];
return (multiple ? ['item1', 'item2', 'item3'] : 'item1') as ConditionalType;
}
// multiple = true
const multiple = discovery(true);
// ^ const multiple: string[]
// multiple = false
const single = discovery(false);
// ^ const single: string
Dans cet exemple, la fonction discovery prend un paramètre multiple, qui détermine si le type de retour est une seule chaîne ou un tableau de chaînes.
C'est génial ! Cependant, lors de l'utilisation dans un composant, nous aimerions également avoir l'option d'omettre complètement le paramètre (attribut). Comme ceci:
<MultiSelect ... />
<MultiSelect ... multiple />
Améliorons la convivialité de notre fonction en permettant à l'argument d'être omis. Pour y parvenir, nous pouvons marquer l'argument avec un point d'interrogation (?) pour indiquer qu'il est optionnel et ajouter une troisième option à notre type conditionnel: undefined. Cela garantira que si rien n'est spécifié, la fonction retourne également un seul élément.
function discovery<T extends boolean | undefined>(multiple?: T) {
type ConditionalType = undefined extends T
? string
: T extends false
? string
: string[];
return (multiple ? ['item1', 'item2', 'item3'] : 'item1') as ConditionalType;
}
// multiple = true
const multiple = discovery(true);
// ^ const multiple: string[]
// multiple = false
const single = discovery(false);
// ^ const single: string
// multiple non présent
const defaultSingle = discovery();
// ^ const defaultSingle: string
Notez que nous avons dû inverser la vérification lors de l'introduction d'undefined (T extends undefined à undefined extends T). Pour comprendre pourquoi, lisez: Extra: Pourquoi undefined extends T et non T extends undefined?
Appliquer les génériques dans les composants Vue
Appliquons maintenant ce concept à un composant Vue. Nous allons créer un composant VSelect qui peut gérer à la fois des sélections simples et multiples en fonction d'une prop multiple.
<script lang="ts" setup generic="TMultiple extends boolean | undefined">
import { computed } from 'vue';
// Retournons le type correct en fonction de la valeur TMultiple.
// - TMultiple === undefined => string
// - TMultiple === false => string
// - TMultiple === true => string[]
type TSingleOrMultiple = undefined extends TMultiple
? string
: TMultiple extends false
? string
: string[];
interface Props {
modelValue?: TSingleOrMultiple
options?: { key: string, value: string }[]
multiple?: TMultiple
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: TSingleOrMultiple): void
}>();
// Note: un attribut vide résultera en une valeur de chaîne vide "", donc nous vérifions explicitement false et undefined
const isMultiple = computed(
() => props.multiple !== false && props.multiple !== undefined,
);
const value = ref<any>(props.modelValue);
watch(value, v => emit('update:modelValue', v));
</script>
<template>
<select v-model="value" :multiple="multiple">
<option disabled value="">
Veuillez sélectionner {{ isMultiple ? "plusieurs" : "un" }}
</option>
<option v-for="{ key, value: optionValue } in options" :key="key" :value="key">
{{ optionValue }}
</option>
</select>
</template>
Dans cet exemple, le composant VSelect adapte son comportement en fonction de la valeur de la prop multiple, grâce à l'utilisation de génériques et de types conditionnels.
Exemple pratique utilisant ElementPlus
La bibliothèque d'interface ElementPlus fournit un composant Select avec une prop multiple. Utilisons ce que nous avons appris jusqu'à présent pour rendre ce composant type-safe. De plus, ce composant permet de spécifier n'importe quel type de tableau d'objets comme options.
Pour activer ce comportement, ils ont fait leur modelValue de type any. Améliorons cela également et ajoutons une assistance supplémentaire pour choisir la propriété optionValue.
Voici un exemple de ce que nous aimerions accomplir:

<script
lang="ts"
setup
generic="
TMultiple extends boolean | undefined,
TOptionType,
// Comme nous ne pouvons pas utiliser undefined comme keyof type, nous utilisons
// une solution de contournement pour spécifier simplement any et ensuite le vérifier
// pour undefined
TOptionValue extends keyof TOptionType = any
"
>
import { computed } from 'vue';
// Vérifions si nous devons retourner l'objet option entier
// ou juste une propriété de cet objet
type TReturnType = undefined extends TOptionValue
? TOptionType
: TOptionType[TOptionValue];
// Ensuite, nous vérifions si nous devons retourner une seule valeur
// ou un tableau de valeurs
type TSingleOrMultiple = undefined extends TMultiple
? TReturnType
: TMultiple extends false
? TReturnType
: TReturnType[];
interface Props {
modelValue?: TSingleOrMultiple
optionValue?: TOptionValue
optionLabel?: keyof TOptionType
options?: TOptionType[]
multiple?: TMultiple
placeholder?: string
disabled?: boolean
clearable?: boolean
filterable?: boolean
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: TSingleOrMultiple): void
}>();
// Note: un attribut vide résultera en une valeur de chaîne vide "",
// donc nous vérifions explicitement false et undefined
const isMultiple = computed(
() => props.multiple !== false && props.multiple !== undefined,
);
function update(value: unknown) {
emit('update:modelValue', value as TSingleOrMultiple);
}
// Function to help work with ElementPlus
function getAsString(value: unknown): string {
return value as string;
}
</script>
<template>
<ElSelect
:modelValue="(modelValue as any)"
:multiple="isMultiple"
:placeholder="placeholder"
:disabled="disabled"
:clearable="clearable"
:filterable="filterable"
@update:modelValue="update"
>
<ElOption
v-for="(option, index) in options"
:key="index"
:label="getAsString(props.optionLabel ? option[props.optionLabel!] : option)"
:value="getAsString(props.optionValue ? option[props.optionValue!] : option)"
/>
</ElSelect>
</template>
<script lang="ts" setup>
import VSelect from './VSelect.vue';
interface Country { id: number, name: string, code: string }
const countries: Country[] = [
{ id: 1, name: 'États-Unis', code: 'US' },
{ id: 2, name: 'Canada', code: 'CA' },
{ id: 3, name: 'Royaume-Uni', code: 'GB' },
{ id: 4, name: 'Australie', code: 'AU' },
{ id: 5, name: 'Allemagne', code: 'DE' },
{ id: 6, name: 'France', code: 'FR' },
{ id: 7, name: 'Japon', code: 'JP' },
{ id: 8, name: 'Chine', code: 'CN' },
{ id: 9, name: 'Inde', code: 'IN' },
{ id: 10, name: 'Brésil', code: 'BR' },
{ id: 11, name: 'Pays-Bas', code: 'NL' },
];
const selectedCountry = ref<string | undefined>();
const selectedCountries = ref<number[]>([]);
</script>
<template>
<div class="form-field">
<label>Sélection simple</label>
<VSelect
v-model="selectedCountry"
:options="countries"
optionLabel="name"
optionValue="code"
/>
<span>
Pays sélectionné:
<pre>{{ selectedCountry }}</pre>
</span>
</div>
<div class="form-field">
<label>Sélection multiple</label>
<VSelect
v-model="selectedCountries"
:options="countries"
optionLabel="name"
optionValue="id"
multiple
/>
<span>
Pays sélectionnés:
<pre>{{ selectedCountries.join(", ") }}</pre>
</span>
</div>
</template>
Pour accomplir cela, nous avons effectué les étapes suivantes:
- Importer les composants ElementPlus: Nous avons importé les deux composants ElementPlus et les avons affichés en fonction de la propriété multiple.
- Créer un type générique: Nous avons créé un type générique pour TOptionValue, qui est un keyof du TOptionType.
- Définir TReturnType: Nous avons ensuite créé un TReturnType qui retourne soit TOptionType soit l'une de ses propriétés.
- Développer le type TSingleOrMultiple: Ce TReturnType est utilisé pour créer un type TSingleOrMultiple basé sur la propriété multiple.
- Utiliser les types dans Props et Emit: Enfin, tous ces types sont utilisés dans les props et les fonctions emit pour fournir une assistance supplémentaire.
Conclusion
L'utilisation de génériques et de types conditionnels dans Vue 3.3 vous permet de créer des composants flexibles et réutilisables qui s'adaptent à différents cas d'utilisation sans ajouter de props ou de complexité inutiles. Cette approche offre plusieurs avantages:
- Rend votre code plus propre et plus maintenable.
- Améliore la sécurité des types, réduisant les erreurs potentielles à l'exécution.
- Améliore l'expérience globale du développeur en fournissant un comportement plus clair et plus prévisible.
Si vous voulez en savoir plus sur Vue 3.3 et les génériques, consultez la documentation officielle ou essayez de mettre en œuvre des modèles similaires dans vos propres projets.
Extra: Pourquoi undefined extends T et non T extends undefined?
Lorsque vous voulez effectuer une vérification de type conditionnel impliquant undefined, il est utile de penser en termes de savoir si T inclut undefined. C'est ce que vérifie undefined extends T—il demande si undefined est un sous-type possible de T.
Si vous deviez utiliser T extends undefined, cela ne retournerait vrai que si T lui-même est exactement undefined. Ce n'est pas la même chose que de vérifier si undefined pourrait faire partie de T.
- undefined extends T: Cela vérifie si undefined fait partie des valeurs possibles de T. En d'autres termes, il retournera vrai si T pourrait être undefined (par exemple, T est true | false | undefined).
- T extends undefined: Cela vérifie si T est précisément undefined. Si T a d'autres valeurs, comme true ou false, cette condition serait évaluée à false.
