Wie können wir den Typ einer Property basierend auf dem Wert einer anderen Property ändern? Zum Beispiel standardmäßig ein einzelnes Element zurückgeben und ein Array von Elementen, wenn eine multiple-Property gesetzt ist.
Um zu verdeutlichen, was ich meine, sehen Sie sich die folgende Aufzeichnung an. Hier können Sie beobachten, wie sich der Typ basierend auf dem multiple-Attribut ändert.

Es gibt Fälle, in denen Sie Ihre Komponente sowohl für Mehrfach- als auch für Einzelwerte wiederverwenden möchten. Sie möchten jedoch keine weitere Property oder ein zusätzliches Emit-Event einführen.
Bei einer Select/MultiSelect-Komponente können Sie diese beispielsweise intelligenter gestalten, indem Sie ihr erlauben, ein Array von Elementen zurückzugeben, wenn multiple angegeben ist, und ein einzelnes Element, wenn nicht.
Lesen Sie weiter, um zu erfahren, wie ich dies durch die Nutzung von Generics in Vue 3.3 erreicht habe. Generics ermöglichten es mir, den Typ einer Property dynamisch basierend auf dem Wert einer anderen Property zu ändern.
Warum Generics in Vue 3.3 verwenden?
Generics in Vue 3.3 ermöglichen die Erstellung flexibler und wiederverwendbarer Komponenten, die sich an unterschiedliche Szenarien anpassen können. Durch die Verwendung von Generics können Sie den Typ einer Property bedingt basierend auf den bereitgestellten Parametern modifizieren, wodurch die Notwendigkeit für redundanten Code oder zusätzliche Props reduziert wird. Denken Sie zum Beispiel an eine Komponente, die sowohl einzelne als auch mehrere Werte abhängig von einem Flag verarbeiten kann – diese Art von Flexibilität macht Komponenten wesentlich vielseitiger und leistungsfähiger.
Das Konzept verstehen
TypeScript kann automatisch einen generischen Typ aus einem Parameter in einer generischen Funktion ableiten. Dieser Typ kann dann verwendet werden, um den Typ eines anderen Parameters oder sogar den Rückgabetyp zu bestimmen. Durch die Kombination mit bedingter Typisierung können Sie eine generische Funktion erstellen, die basierend auf dem bereitgestellten Parameter einen anderen Wert zurückgibt.
Generics in Komponenten funktionieren ähnlich wie in regulären Funktionen. Um dies besser zu verstehen, arbeiten wir zunächst ein Beispiel mit reinem TypeScript durch, bevor wir es in unsere Vue-Komponente integrieren.
TypeScript-Beispiel
// Wir leiten den Rückgabetyp vom Argumenttyp ab.
function discovery<T>(arg: T): T {
return arg;
}
// String rein, String raus
const result = discovery('Hello, World!' als string);
// ^ const result: string
Jetzt können wir die Möglichkeiten dieses generischen Typs eingrenzen, indem wir extends verwenden, wodurch wir einen neuen bedingten Typ basierend auf angegebenen Optionen erstellen können.
Im folgenden Beispiel beschränke ich die Möglichkeiten auf nur true und false. Als Ergebnis gibt es ein Array von Werten zurück, wenn true, und einen einzelnen Wert, wenn 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 diesem Beispiel nimmt die discovery-Funktion einen Parameter multiple, der bestimmt, ob der Rückgabetyp ein einzelner String oder ein Array von Strings ist.
Das ist großartig! Wenn wir dies jedoch in einer Komponente verwenden, möchten wir auch die Möglichkeit haben, den Parameter (das Attribut) vollständig wegzulassen. So:
<MultiSelect ... />
<MultiSelect ... multiple />
Lassen Sie uns die Benutzerfreundlichkeit unserer Funktion verbessern, indem wir erlauben, dass das Argument weggelassen werden kann. Um dies zu erreichen, können wir das Argument mit einem Fragezeichen (?) kennzeichnen, um anzuzeigen, dass es optional ist, und eine dritte Option zu unserem bedingten Typ hinzufügen: undefined. Dies stellt sicher, dass die Funktion auch dann ein einzelnes Element zurückgibt, wenn nichts angegeben ist.
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 nicht vorhanden
const defaultSingle = discovery();
// ^ const defaultSingle: string
Beachten Sie, dass wir die Prüfung umkehren mussten, als wir undefined einführten (T extends undefined zu undefined extends T). Um zu verstehen, warum, lesen Sie: Extra: Warum undefined extends T und nicht T extends undefined?
Generics in Vue-Komponenten anwenden
Wenden wir nun dieses Konzept auf eine Vue-Komponente an. Wir erstellen eine VSelect-Komponente, die sowohl Einzel- als auch Mehrfachauswahlen abhängig von einer multiple-Prop verarbeiten kann.
<script lang="ts" setup generic="TMultiple extends boolean | undefined">
import { computed } from 'vue';
// Geben wir den korrekten Typ basierend auf dem TMultiple-Wert zurück.
// - 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
}>();
// Hinweis: Ein leeres Attribut führt zu einem leeren String "" Wert, daher prüfen wir explizit auf false und 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="">
Bitte wählen Sie {{ isMultiple ? "mehrere" : "eine" }}
</option>
<option v-for="{ key, value: optionValue } in options" :key="key" :value="key">
{{ optionValue }}
</option>
</select>
</template>
In diesem Beispiel passt die VSelect-Komponente ihr Verhalten abhängig vom Wert der multiple-Prop an, dank der Verwendung von Generics und bedingten Typen.
Praktisches Beispiel mit ElementPlus
Die UI-Bibliothek ElementPlus bietet eine Select-Komponente mit einer multiple-Prop. Lassen Sie uns das bisher Gelernte nutzen, um diese Komponente type-safe zu machen. Zusätzlich ermöglicht diese Komponente die Angabe eines beliebigen Objektarray-Typs als Optionen.
Um dieses Verhalten zu ermöglichen, haben sie ihren modelValue vom Typ any gemacht. Lassen Sie uns dies ebenfalls verbessern und zusätzliche Unterstützung bei der Auswahl der optionValue-Property hinzufügen.
Hier ist ein Beispiel dessen, was wir erreichen möchten:

<script
lang="ts"
setup
generic="
TMultiple extends boolean | undefined,
TOptionType,
// Da wir undefined nicht als keyof-Typ verwenden können, nutzen wir
// einen Workaround, um einfach any anzugeben und es dann
// auf undefined zu prüfen
TOptionValue extends keyof TOptionType = any
"
>
import { computed } from 'vue';
// Prüfen, ob wir das gesamte Options-Objekt zurückgeben müssen
// oder nur eine Property dieses Objekts
type TReturnType = undefined extends TOptionValue
? TOptionType
: TOptionType[TOptionValue];
// Dann prüfen wir, ob wir einen einzelnen Wert zurückgeben müssen
// oder ein Array von Werten
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
}>();
// Hinweis: Ein leeres Attribut führt zu einem leeren String "" Wert,
// daher prüfen wir explizit auf false und 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: 'Vereinigte Staaten', code: 'US' },
{ id: 2, name: 'Kanada', code: 'CA' },
{ id: 3, name: 'Vereinigtes Königreich', code: 'GB' },
{ id: 4, name: 'Australien', code: 'AU' },
{ id: 5, name: 'Deutschland', code: 'DE' },
{ id: 6, name: 'Frankreich', code: 'FR' },
{ id: 7, name: 'Japan', code: 'JP' },
{ id: 8, name: 'China', code: 'CN' },
{ id: 9, name: 'Indien', code: 'IN' },
{ id: 10, name: 'Brasilien', code: 'BR' },
{ id: 11, name: 'Niederlande', code: 'NL' },
];
const selectedCountry = ref<string | undefined>();
const selectedCountries = ref<number[]>([]);
</script>
<template>
<div class="form-field">
<label>Einzelauswahl</label>
<VSelect
v-model="selectedCountry"
:options="countries"
optionLabel="name"
optionValue="code"
/>
<span>
Ausgewähltes Land:
<pre>{{ selectedCountry }}</pre>
</span>
</div>
<div class="form-field">
<label>Mehrfachauswahl</label>
<VSelect
v-model="selectedCountries"
:options="countries"
optionLabel="name"
optionValue="id"
multiple
/>
<span>
Ausgewählte Länder:
<pre>{{ selectedCountries.join(", ") }}</pre>
</span>
</div>
</template>
Um dies zu erreichen, haben wir die folgenden Schritte durchgeführt:
- ElementPlus-Komponenten importieren: Wir haben beide ElementPlus-Komponenten importiert und sie basierend auf der multiple-Property angezeigt.
- Generischen Typ erstellen: Wir haben einen generischen Typ für TOptionValue erstellt, der ein keyof des TOptionType ist.
- TReturnType definieren: Wir haben dann einen TReturnType erstellt, der entweder TOptionType oder eine seiner Properties zurückgibt.
- TSingleOrMultiple-Typ entwickeln: Dieser TReturnType wird verwendet, um einen TSingleOrMultiple-Typ basierend auf der multiple-Property zu erstellen.
- Typen in Props und Emit verwenden: Schließlich werden alle diese Typen in den Props- und Emit-Funktionen verwendet, um zusätzliche Unterstützung zu bieten.
Fazit
Die Verwendung von Generics und bedingten Typen in Vue 3.3 ermöglicht es Ihnen, flexible, wiederverwendbare Komponenten zu erstellen, die sich an unterschiedliche Anwendungsfälle anpassen, ohne unnötige Props oder Komplexität hinzuzufügen. Dieser Ansatz bietet mehrere Vorteile:
- Macht Ihren Code sauberer und wartbarer.
- Verbessert die Typsicherheit und reduziert potenzielle Laufzeitfehler.
- Verbessert die allgemeine Entwicklererfahrung durch klareres und vorhersehbareres Verhalten.
Wenn Sie mehr über Vue 3.3 und Generics erfahren möchten, schauen Sie sich die offizielle Dokumentation an oder versuchen Sie, ähnliche Muster in Ihren eigenen Projekten zu implementieren.
Extra: Warum undefined extends T und nicht T extends undefined?
Wenn Sie eine bedingte Typprüfung mit undefined durchführen möchten, ist es hilfreich, in Begriffen zu denken, ob T undefined einschließt. Das ist es, was undefined extends T prüft – es fragt, ob undefined ein möglicher Subtyp von T ist.
Wenn Sie T extends undefined verwenden würden, würde es nur true zurückgeben, wenn T selbst genau undefined ist. Das ist nicht dasselbe wie zu prüfen, ob undefined Teil von T sein könnte.
- undefined extends T: Dies prüft, ob undefined Teil der möglichen Werte von T ist. Mit anderen Worten, es gibt true zurück, wenn T undefined sein könnte (z. B. T ist true | false | undefined).
- T extends undefined: Dies prüft, ob T genau undefined ist. Wenn T andere Werte hat, wie true oder false, würde diese Bedingung zu false ausgewertet werden.
