Hoe kunnen we het type van een property veranderen op basis van de waarde van een andere property? Bijvoorbeeld, standaard een enkel item retourneren, en een array van items wanneer een multiple property is ingesteld.
Om te demonstreren wat ik bedoel, zie de opname hieronder. Hier kun je zien hoe het type verandert op basis van het multiple attribuut.

Er zijn gevallen waarin je je component wilt hergebruiken voor zowel meerdere als enkele waarden. Je wilt echter geen extra property of emit event introduceren.
Bijvoorbeeld, met een Select/MultiSelect component kun je deze slimmer maken door het een array van items te laten retourneren wanneer multiple is gespecificeerd en een enkel item wanneer dat niet het geval is.
Lees verder om te zien hoe ik dit heb bereikt door gebruik te maken van generics in Vue 3.3. Generics stelden me in staat om dynamisch het type van een property te veranderen op basis van de waarde van een andere property.
Waarom Generics gebruiken in Vue 3.3?
Generics in Vue 3.3 maken het mogelijk om flexibele en herbruikbare componenten te creëren die zich kunnen aanpassen aan verschillende scenario's. Door generics te gebruiken, kun je voorwaardelijk het type van een property aanpassen op basis van de meegegeven parameters, waardoor de noodzaak voor redundante code of extra props verminderd wordt. Denk bijvoorbeeld aan een component die zowel enkele als meerdere waarden kan verwerken afhankelijk van een vlag—dit soort flexibiliteit maakt componenten veel veelzijdiger en krachtiger.
Het concept begrijpen
TypeScript kan automatisch een generiek type afleiden van een parameter in een generieke functie. Dit type kan vervolgens worden gebruikt om het type van een andere parameter of zelfs het return type te bepalen. Door dit te combineren met conditionele typing, kun je een generieke functie creëren die een andere waarde retourneert op basis van de meegegeven parameter.
Generics in componenten werken vergelijkbaar met die in reguliere functies. Om dit beter te begrijpen, laten we eerst een voorbeeld doorlopen met pure TypeScript voordat we het integreren in onze Vue component.
TypeScript Voorbeeld
// We leiden het return type af van het argument type.
function discovery<T>(arg: T): T {
return arg;
}
// String in, string uit
const result = discovery('Hallo, Wereld!' as string);
// ^ const result: string
Nu kunnen we de mogelijkheden van dit generieke type beperken door extends te gebruiken, waarmee we een nieuw conditioneel type kunnen creëren op basis van gespecificeerde opties.
In het volgende voorbeeld beperk ik de mogelijkheden tot alleen true en false. Als resultaat retourneert het een array van waarden wanneer true en een enkele waarde wanneer 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
In dit voorbeeld neemt de discovery functie een parameter multiple, die bepaalt of het return type een enkele string of een array van strings is.
Dit is geweldig! Wanneer we dit echter in een component gebruiken, willen we ook de optie hebben om de parameter (attribuut) volledig weg te laten. Zoals dit:
<MultiSelect ... />
<MultiSelect ... multiple />
Laten we de bruikbaarheid van onze functie verbeteren door toe te staan dat het argument wordt weggelaten. Om dit te bereiken, kunnen we het argument markeren met een vraagteken (?) om aan te geven dat het optioneel is en een derde optie toevoegen aan ons conditionele type: undefined. Dit zorgt ervoor dat als er niets wordt gespecificeerd, de functie ook een enkel item retourneert.
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 niet aanwezig
const defaultSingle = discovery();
// ^ const defaultSingle: string
Merk op dat we de check moesten omdraaien bij het introduceren van undefined (T extends undefined naar undefined extends T). Om te begrijpen waarom, lees: Extra: Waarom undefined extends T en niet T extends undefined?
Generics toepassen in Vue Componenten
Laten we dit concept nu toepassen op een Vue component. We zullen een VSelect component creëren die zowel enkele als meerdere selecties kan verwerken afhankelijk van een multiple prop.
<script lang="ts" setup generic="TMultiple extends boolean | undefined">
import { computed } from 'vue';
// Laten we het juiste type retourneren op basis van de TMultiple waarde.
// - 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
}>();
// Opmerking: een leeg attribuut resulteert in een lege string "" waarde, daarom checken we expliciet op false en 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="">
Selecteer {{ isMultiple ? "meerdere" : "één" }}
</option>
<option v-for="{ key, value: optionValue } in options" :key="key" :value="key">
{{ optionValue }}
</option>
</select>
</template>
In dit voorbeeld past de VSelect component zijn gedrag aan afhankelijk van de waarde van de multiple prop, dankzij het gebruik van generics en conditionele types.
Praktisch Voorbeeld met ElementPlus
De UI bibliotheek ElementPlus biedt een Select component met een multiple prop. Laten we gebruiken wat we tot nu toe hebben geleerd om deze component type-safe te maken. Daarnaast maakt deze component het mogelijk om elk type object array als opties te specificeren.
Om dit gedrag mogelijk te maken, hebben ze hun modelValue van het type any gemaakt. Laten we dat ook verbeteren en wat extra hulp toevoegen bij het kiezen van de optionValue property.
Hier is een voorbeeld van wat we willen bereiken:

<script
lang="ts"
setup
generic="
TMultiple extends boolean | undefined,
TOptionType,
// Aangezien we undefined niet kunnen gebruiken als een keyof type, gebruiken
// we een workaround om gewoon any te specificeren en het vervolgens
// te checken voor undefined
TOptionValue extends keyof TOptionType = any
"
>
import { computed } from 'vue';
// Controleer of we het hele optie object moeten retourneren
// of alleen een property van dat object
type TReturnType = undefined extends TOptionValue
? TOptionType
: TOptionType[TOptionValue];
// Vervolgens controleren we of we een enkele waarde moeten retourneren
// of een array van waarden
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
}>();
// Opmerking: een leeg attribuut resulteert in een lege string "" waarde,
// daarom checken we expliciet op false en 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: 'Verenigde Staten', code: 'US' },
{ id: 2, name: 'Canada', code: 'CA' },
{ id: 3, name: 'Verenigd Koninkrijk', code: 'GB' },
{ id: 4, name: 'Australië', code: 'AU' },
{ id: 5, name: 'Duitsland', code: 'DE' },
{ id: 6, name: 'Frankrijk', code: 'FR' },
{ id: 7, name: 'Japan', code: 'JP' },
{ id: 8, name: 'China', code: 'CN' },
{ id: 9, name: 'India', code: 'IN' },
{ id: 10, name: 'Brazilië', code: 'BR' },
{ id: 11, name: 'Nederland', code: 'NL' },
];
const selectedCountry = ref<string | undefined>();
const selectedCountries = ref<number[]>([]);
</script>
<template>
<div class="form-field">
<label>Enkele selectie</label>
<VSelect
v-model="selectedCountry"
:options="countries"
optionLabel="name"
optionValue="code"
/>
<span>
Geselecteerd land:
<pre>{{ selectedCountry }}</pre>
</span>
</div>
<div class="form-field">
<label>Meerdere selectie</label>
<VSelect
v-model="selectedCountries"
:options="countries"
optionLabel="name"
optionValue="id"
multiple
/>
<span>
Geselecteerde landen:
<pre>{{ selectedCountries.join(", ") }}</pre>
</span>
</div>
</template>
Om dit te bereiken, hebben we de volgende stappen uitgevoerd:
- ElementPlus Componenten Importeren: We hebben beide ElementPlus componenten geïmporteerd en ze weergegeven op basis van de multiple property.
- Een Generiek Type Creëren: We hebben een generiek type gemaakt voor TOptionValue, die een keyof is van de TOptionType.
- TReturnType Definiëren: We hebben vervolgens een TReturnType gemaakt die ofwel TOptionType retourneert of één van zijn properties.
- TSingleOrMultiple Type Ontwikkelen: Deze TReturnType wordt gebruikt om een TSingleOrMultiple type te creëren op basis van de multiple property.
- Types Gebruiken in Props en Emit: Ten slotte worden al deze types gebruikt in de props en emit functies om extra hulp te bieden.
Conclusie
Het gebruik van generics en conditionele types in Vue 3.3 stelt je in staat om flexibele, herbruikbare componenten te creëren die zich aanpassen aan verschillende use cases zonder onnodige props of complexiteit toe te voegen. Deze aanpak biedt verschillende voordelen:
- Maakt je code schoner en beter onderhoudbaar.
- Verbetert type safety, waardoor potentiële runtime fouten worden verminderd.
- Verbetert de algehele ontwikkelaarservaring door duidelijker en voorspelbaarder gedrag te bieden.
Als je meer wilt ontdekken over Vue 3.3 en generics, bekijk dan de officiële documentatie of probeer vergelijkbare patronen te implementeren in je eigen projecten.
Extra: Waarom undefined extends T en niet T extends undefined?
Wanneer je een conditionele type check wilt uitvoeren met undefined, is het nuttig om te denken in termen van of T undefined bevat. Dit is wat undefined extends T controleert—het vraagt of undefined een mogelijk subtype is van T.
Als je T extends undefined zou gebruiken, zou het alleen true retourneren als T zelf precies undefined is. Dit is niet hetzelfde als controleren of undefined onderdeel zou kunnen zijn van T.
- undefined extends T: Dit controleert of undefined onderdeel is van de mogelijke waarden van T. Met andere woorden, het retourneert true als T undefined zou kunnen zijn (bijv. T is true | false | undefined).
- T extends undefined: Dit controleert of T precies undefined is. Als T andere waarden heeft, zoals true of false, zou deze conditie evalueren naar false.
