Vue 3 mit TypeScript im Jahr 2026: Typsichere Props, Emits und Composables
Typsichere Vue-3-Komponenten mit TypeScript meistern: generisches defineProps, defineEmits als Tuple, typisierte Composables, defineModel und InjectionKey samt Interviewfragen.

Vue 3 mit TypeScript liefert im Jahr 2026 eine vollständig statische Typprüfung über Props, Events und wiederverwendbare Logik hinweg. Die Syntax <script setup> in Kombination mit Compiler-Makros wie defineProps und defineEmits verwandelt frühere Objektdefinitionen, die nur zur Laufzeit galten, in Verträge zur Kompilierzeit. Dieser Leitfaden behandelt die typsicheren Muster, die jede Vue-Entwicklerin und jeder Vue-Entwickler für produktiven Code und technische Interviews benötigt.
In Vue 3.5+ leitet das generische defineProps<T>() die Prop-Typen direkt aus einem TypeScript-Interface ab — keine type-Felder zur Laufzeit, keine PropType-Casts. In Verbindung mit reaktivem Prop-Destructuring stehen Standardwerte inline: const { size = 'md' } = defineProps<Props>().
Props mit defineProps und TypeScript-Interfaces typisieren
Die Laufzeit-Deklaration von Props (props: { title: String }) funktioniert, dupliziert jedoch Typinformationen und verliert an Präzision. Union-Typen, verschachtelte Objekte und Funktionssignaturen lassen sich damit nicht sauber ausdrücken. Die generische Form von defineProps löst dieses Problem, indem sie die Struktur aus einem TypeScript-Typ übernimmt, den der Vue-Compiler anschließend automatisch in die korrekte Laufzeit-Deklaration übersetzt.
<!-- UserCard.vue -->
<script setup lang="ts">
interface Props {
userId: number // required primitive
name: string
role: 'admin' | 'member' // union type, impossible with runtime props
tags?: string[] // optional array
onSelect?: (id: number) => void // typed callback prop
}
const props = defineProps<Props>()
</script>
<template>
<article @click="props.onSelect?.(props.userId)">
<h3>{{ props.name }}</h3>
<span>{{ props.role }}</span>
</article>
</template>Die Prop role ist nun auf zwei literale Zeichenketten beschränkt, sodass das Übergeben von role="guest" den Build scheitern lässt. Genau das ist der Kerngrund, zu defineProps mit TypeScript zu greifen: Der offizielle Vue-TypeScript-Leitfaden behandelt die generische Form als Standard für Projekte mit <script setup>.
Standardwerte für Props: withDefaults vs. reaktives Destructuring
Reine Typ-Props verfügen für sich genommen über keinen Laufzeit-Mechanismus für Standardwerte. Historisch lautete die Antwort withDefaults, das das Makro umschließt und ein Objekt mit Standardwerten zusammenführt:
<!-- Button.vue -->
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
}
// withDefaults keeps reactivity and applies fallback values
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
disabled: false,
})
</script>Vue 3.5 hat das reaktive Prop-Destructuring stabilisiert, das inzwischen das prägnantere Muster darstellt. Wird das Ergebnis von defineProps destrukturiert und im selben Ausdruck ein Standardwert zugewiesen, bleibt der Zugriff vollständig reaktiv — der Compiler schreibt jeden Zugriff im Hintergrund wieder auf props.x um.
<!-- Badge.vue -->
<script setup lang="ts">
interface Props {
label: string
color?: 'green' | 'red'
outlined?: boolean
}
// Defaults are declared inline; each variable stays reactive
const { label, color = 'green', outlined = false } = defineProps<Props>()
</script>
<template>
<span :class="[color, { outlined }]">{{ label }}</span>
</template>Destrukturierte Props bleiben im Template und in computed reaktiv, aber das direkte Übergeben eines destrukturierten Werts an watch oder ein Composable liest ihn nur einmal und trennt die reaktive Verbindung. Für solche Fälle empfiehlt sich ein Getter — watch(() => color, ...) — oder die Umwandlung mit toRef(props, 'color'), wenn nachgelagert eine Ref benötigt wird.
Dies ist eine häufige Interview-Falle: Kandidatinnen und Kandidaten nehmen an, die destrukturierte Variable sei ein einfacher Wert, während der Compiler in Wahrheit jeden Lesezugriff wieder auf props.color umgeleitet hat.
Typsichere Events mit defineEmits
Events verdienen dieselbe Sorgfalt wie Props. Das generische defineEmits beschreibt jeden Event-Namen und dessen Nutzlast als Tuple und liefert der Elternkomponente Autovervollständigung sowie der Kindkomponente Prüfungen zur Kompilierzeit, dass die richtigen Argumente ausgelöst werden.
<!-- SearchInput.vue -->
<script setup lang="ts">
// Tuple syntax: event name -> [ ...payload types ]
const emit = defineEmits<{
search: [query: string]
clear: [] // no payload
select: [id: number, label: string] // multiple args
}>()
function onSubmit(value: string) {
emit('search', value) // ✅ typed
// emit('search', 42) // ❌ compile error: number not assignable to string
}
</script>Diese Tuple-Form ersetzte die ältere Aufrufsignatur-Syntax ((e: 'search', q: string): void), weil sie besser lesbar ist und mehrere Events ohne Überladungen unterstützt. Wenn ein Event keine Daten transportiert, dokumentiert ein leeres Tuple [] dies ausdrücklich. Die Kombination typisierter Events mit typisierten Props erzeugt Komponenten, deren gesamte öffentliche Schnittstelle vor der Laufzeit überprüfbar ist — dieselbe Disziplin, die auch im Leitfaden zur Vue Composition API behandelt wird.
Composables für wiederverwendbare Logik typisieren
Composables sind gewöhnliche Funktionen und folgen daher den normalen TypeScript-Regeln — einige Konventionen halten sie jedoch ergonomisch. Es empfiehlt sich, Ref-Typen explizit zurückzugeben, wenn die Inferenz nicht offensichtlich ist, und Generics zu verwenden, wenn ein Composable beliebige Daten wie eine API-Antwort kapselt.
import { ref, type Ref } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
loading: Ref<boolean>
}
// Generic <T> flows through to the caller's typed data
export function useFetch<T>(url: string): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const loading = ref(true)
fetch(url)
.then((r) => r.json())
.then((json: T) => { data.value = json })
.catch((e: Error) => { error.value = e })
.finally(() => { loading.value = false })
return { data, error, loading }
}An der Aufrufstelle macht der Generics-Parameter data vollständig typisiert, ganz ohne zusätzliche Annotation:
interface User { id: number; name: string }
// data is Ref<User[] | null> — inferred from the generic
const { data: users, loading } = useFetch<User[]>('/api/users')Die explizite Annotation des Rückgabeobjekts (UseFetchReturn<T>) ist die wenigen zusätzlichen Zeilen wert: Sie dokumentiert den Vertrag, verhindert das versehentliche Leaken interner Refs und liefert Konsumenten einen einzigen Typ zum Importieren. Für tiefergehende Composable-Muster wie Argumentüberladungen und lebenszyklusbewusste Bereinigung siehe den Leitfaden zu fortgeschrittenen Vue-Composables.
Bereit für deine Vue.js / Nuxt.js-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
defineModel für bidirektionale Bindung typisieren
defineModel, stabil seit Vue 3.4, fasst die alte Kombination aus modelValue-Prop und update:modelValue-Emit in einer einzigen beschreibbaren Ref zusammen. Der Generics-Parameter typisiert beide Richtungen der Bindung auf einmal.
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// model is Ref<number>; parent v-model is type-checked as number
const model = defineModel<number>({ required: true })
function increment() {
model.value++ // writing back updates the parent
}
</script>
<template>
<input :value="model" type="number" @input="model = Number(($event.target as HTMLInputElement).value)" />
<button @click="increment">+1</button>
</template>Benannte Modelle — defineModel<string>('title') — werden auf v-model:title abgebildet und erhalten dieselbe Typisierung. Damit entfällt eine ganze Klasse von Fehlern durch nicht zueinander passende Nutzlasten, die das manuelle Muster aus Prop und Emit früher verbarg.
Template-Refs und Komponenteninstanzen typisieren
Der Zugriff auf einen DOM-Knoten oder eine Kindkomponente über eine ref ist die Stelle, an der untypisierter Vue-Code am häufigsten auf any zurückfällt. Die Lösung besteht darin, useTemplateRef (Vue 3.5+) oder die ref selbst mit dem Elementtyp zu parametrisieren, sodass der Eigenschaftszugriff gegen die echte DOM-Schnittstelle geprüft wird.
<!-- FocusField.vue -->
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
// The ref is typed as HTMLInputElement | null
const inputRef = useTemplateRef<HTMLInputElement>('field')
onMounted(() => {
// .focus() and .select() are known because the element type is explicit
inputRef.value?.focus()
})
</script>
<template>
<input ref="field" placeholder="Type to search" />
</template>Für eine Referenz auf eine Kindkomponente extrahiert InstanceType<typeof Child> den öffentlichen Typ der Komponente und legt offen, was das Kind über defineExpose bereitstellt. So bleiben Methodenaufrufe von der Eltern- zur Kindkomponente vollständig geprüft statt geraten.
<!-- Parent.vue -->
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Modal from './Modal.vue'
// Ref is typed to Modal's exposed API (e.g. open / close methods)
const modal = useTemplateRef<InstanceType<typeof Modal>>('modal')
function launch() {
modal.value?.open() // ✅ checked against Modal's defineExpose
}
</script>Typsicheres provide und inject mit InjectionKey
Dependency Injection über den Komponentenbaum hinweg verliert Typinformationen, sofern der Schlüssel sie nicht mitführt. InjectionKey<T> ist ein typisiertes Symbol, das den Typ eines Werts an seinen Schlüssel bindet, sodass provide und inject ohne manuelle Casts synchron bleiben.
import type { InjectionKey, Ref } from 'vue'
export interface ThemeContext {
mode: Ref<'light' | 'dark'>
toggle: () => void
}
// The key permanently associates the ThemeContext type with this symbol
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')// provider (script setup) — value must match ThemeContext or it fails to compile
provide(ThemeKey, { mode, toggle })
// consumer — theme is inferred as ThemeContext | undefined
const theme = inject(ThemeKey)
theme?.toggle()Das Bereitstellen eines Werts, dessen Struktur nicht zu ThemeContext passt, ist ein Kompilierfehler an der Injektionsstelle und fängt so Abweichungen zwischen weit entfernten Komponenten ab, bevor sie ausgeliefert werden.
Häufige Interviewfragen zu Vue und TypeScript
Interviewende prüfen, ob eine Kandidatin oder ein Kandidat die Grenze zwischen Typen zur Kompilierzeit und Verhalten zur Laufzeit versteht. Einige wiederkehrende Fragen und ihre prägnanten Antworten:
- Warum ist
defineProps<T>()der Laufzeit-Objektform vorzuziehen? Generische Props drücken Union-Typen, Funktionssignaturen und verschachtelte Strukturen aus, die Laufzeit-Deklarationen nicht abbilden können, und sie beseitigen doppelte Typ- und Laufzeitdefinitionen. - Sind destrukturierte Props reaktiv? Ja, in Vue 3.5+, weil der Compiler den Zugriff auf
props.xumschreibt. Doch destrukturierte Werte, die anwatchoder ein Composable übergeben werden, werden nur einmal gelesen — hier hilft ein Getter odertoRef. - Wie typisiert man ein ausgelöstes Event ohne Nutzlast? Mit einem leeren Tuple:
defineEmits<{ close: [] }>(). - Was ersetzt
defineModel? Das Paar ausmodelValue-Prop undupdate:modelValue-Emit, vereinigt in einer einzigen typisierten, beschreibbaren Ref.
Diese Fragen an einer echten Fragensammlung zu üben schärft die Reflexe, die Interviews belohnen — das Interview-Modul zu Vue-Composables trainiert genau diese Muster. Auch das Tooling zählt: vue-tsc in der CI ausführen, damit Typfehler Merges blockieren, und bei Composable-Generics auf das TypeScript-Handbuch zurückgreifen. Das Editor-Erlebnis wird von Vues offiziellem Volar-Tooling angetrieben, das diese Makros liest, um Fehler inline anzuzeigen.
Fazit
- Generisches
defineProps<Props>()verwenden, um Union-Typen, optionale Felder und Callback-Props auszudrücken, die Laufzeit-Deklarationen nicht erfassen können. - In Vue 3.5+ reaktives Prop-Destructuring mit Inline-Standardwerten bevorzugen und
withDefaultsnur dann heranziehen, wenn ein gemeinsam genutztes Objekt mit Standardwerten übersichtlicher ist. - Events mit der Tuple-Form von
defineEmitstypisieren, damit Nutzlasten sowohl im Kind als auch in der Elternkomponente zur Kompilierzeit geprüft werden. - Rückgabetypen von Composables explizit annotieren und Generics nutzen, um vom Aufrufer gelieferte Datentypen durchgängig weiterzureichen.
defineModel<T>()für bidirektionale Bindung einsetzen, um den Boilerplate aus Prop und Emit auf eine einzige typisierte Ref zu reduzieren.vue-tscin der kontinuierlichen Integration ausführen, damit Typregressionen den Build scheitern lassen, statt in die Produktion zu gelangen.
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
Verwandte Artikel

Fortgeschrittene Vue 3 Composables: Wiederverwendbare Muster und Interviewfragen 2026
Umfassender Leitfaden zu fortgeschrittenen Vue 3 Composables mit wiederverwendbaren Mustern, asynchroner Fehlerbehandlung, Dependency Injection, Formularvalidierung und aktuellen Interviewfragen fuer 2026.

Vue 3 Composition API: Der vollständige Leitfaden zur Reaktivität
Die Vue 3 Composition API praxisnah erklärt: ref, reactive, computed, watch und Composables für leistungsstarke Vue-Anwendungen.

Nuxt 4 in 2026: Neue Verzeichnisstruktur und Migration von Nuxt 3
Nuxt 4 bringt eine neue app/-Verzeichnisstruktur, verbessertes Data-Fetching, Shallow Reactivity und Unhead v2. Kompletter Migrationsleitfaden von Nuxt 3 mit Codemod-Automatisierung.