Zum Inhalt springen

TypeScript mit Vue

TypeScript ist ein typisiertes Superset von JavaScript, das zu JavaScript kompiliert wird. Es bietet statische Typisierung und bessere IDE-Unterstützung.

Vorteile:

  • Typsicherheit - Fehler zur Compile-Zeit
  • IntelliSense - Bessere Autovervollständigung
  • Refactoring - Sichere Code-Änderungen
  • Größere Projekte (> 5 Komponenten)
  • Team-Entwicklung
  • Langlebige Codebasen
  • Komplexe Datenstrukturen
  • API-Integration
  • Wiederverwendbare Libraries
// Primitive Typen
const name: string = 'Max'
const age: number = 30
const isActive: boolean = true
// Arrays
const numbers: number[] = [1, 2, 3]
const names: Array<string> = ['Max', 'Anna']
// Objekte
const user: { name: string; age: number } = {
name: 'Max',
age: 30
}
// Union Types
let id: string | number = 123
id = 'abc' // Auch erlaubt
// Literal Types
type Status = 'pending' | 'active' | 'done'
const status: Status = 'active'

TypeScript kann Typen oft automatisch ableiten - explizite Annotationen sind dann optional:

// TypeScript erkennt die Typen automatisch
const name = 'Max' // string
const age = 30 // number
const isActive = true // boolean
const items = [1, 2, 3] // number[]
// Bei Funktionen: Parameter annotieren, Rückgabe wird abgeleitet
function add(a: number, b: number) {
return a + b // Rückgabe: number (automatisch)
}
// Manchmal explizit nötig
const user = ref<User | null>(null) // Ohne Annotation wäre es ref<null>
interface User {
id: number
name: string
email: string
}
// Erweiterbar
interface Admin extends User {
permissions: string[]
}
// Declaration Merging
interface User {
age?: number // Wird hinzugefügt
}

Generics ermöglichen wiederverwendbare Typen mit Platzhaltern:

// Generische Funktion
function identity<T>(value: T): T {
return value
}
identity<string>('hello') // T = string
identity(42) // T = number (inferiert)
// Generisches Interface
interface ApiResponse<T> {
data: T
status: number
message: string
}
const userResponse: ApiResponse<User> = {
data: { id: 1, name: 'Max', email: 'max@test.de' },
status: 200,
message: 'OK'
}
// Vue nutzt Generics intensiv
const count = ref<number>(0)
const users = ref<User[]>([])
<!-- TypeScript aktivieren mit lang="ts" -->
<script setup lang="ts">
import { ref, computed } from 'vue'
// Typen werden oft automatisch inferiert
const count = ref(0) // Ref<number>
const message = ref('Hello') // Ref<string>
// Explizit wenn nötig
const user = ref<User | null>(null)
const items = ref<string[]>([])
// Computed: Rückgabetyp automatisch
const doubleCount = computed(() => count.value * 2) // ComputedRef<number>
</script>
<script setup lang="ts">
// Interface für Props definieren
interface Props {
title: string
count?: number // Optional mit ?
items: string[]
user: {
id: number
name: string
}
}
// defineProps mit Generic
const props = defineProps<Props>()
// Mit Default-Werten
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [] // Factory für Arrays/Objekte
})
// Zugriff (voll typisiert)
console.log(props.title) // string
console.log(props.count) // number | undefined (oder number mit default)
</script>
<script setup lang="ts">
// Emits mit Payload-Typen
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
(e: 'submit', data: { name: string; email: string }): void
}>()
// Alternativ: Kurzschreibweise (Vue 3.3+)
const emit = defineEmits<{
update: [value: string]
delete: [id: number]
submit: [data: { name: string; email: string }]
}>()
// Verwendung - Typen werden geprüft
emit('update', 'neuer Wert') // OK
emit('update', 123) // Fehler: number statt string
emit('unknown') // Fehler: Event existiert nicht
</script>
<script setup lang="ts">
import { ref, reactive } from 'vue'
// ref() - Generic für komplexe Typen
interface User {
id: number
name: string
}
const user = ref<User | null>(null)
const users = ref<User[]>([])
// reactive() - Typ wird meist inferiert
const state = reactive({
count: 0,
name: 'Max'
})
// Typ: { count: number; name: string }
// Explizit wenn nötig
interface State {
users: User[]
loading: boolean
}
const state = reactive<State>({
users: [],
loading: false
})
</script>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// DOM-Element typisieren
const inputRef = ref<HTMLInputElement | null>(null)
const canvasRef = ref<HTMLCanvasElement | null>(null)
onMounted(() => {
// TypeScript weiß jetzt den Element-Typ
inputRef.value?.focus()
inputRef.value?.select()
const ctx = canvasRef.value?.getContext('2d')
})
// Komponenten-Ref typisieren
import MyComponent from './MyComponent.vue'
const componentRef = ref<InstanceType<typeof MyComponent> | null>(null)
</script>
<template>
<input ref="inputRef" />
<canvas ref="canvasRef"></canvas>
<MyComponent ref="componentRef" />
</template>
composables/useFetch.ts
import { ref, type Ref } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
isLoading: Ref<boolean>
execute: () => Promise<void>
}
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 isLoading = ref(false)
async function execute() {
isLoading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
return { data, error, isLoading, execute }
}

Die Options API hat fundamentale Probleme mit TypeScript wegen this:

<script lang="ts">
import { defineComponent } from 'vue'
// defineComponent() ist PFLICHT für TypeScript
export default defineComponent({
data() {
return {
count: 0,
user: null as User | null // Casting nötig
}
},
computed: {
// this-Typ funktioniert, aber ist komplex
doubleCount(): number {
return this.count * 2
}
},
methods: {
increment() {
this.count++ // this muss von defineComponent inferiert werden
}
}
})
</script>
  • defineComponent() Wrapper nötig
  • this-Typisierung ist fragil
  • Data-Properties brauchen Type Assertions
  • Mixins sind kaum typisierbar
  • Schlechte IDE-Unterstützung in Templates
<script setup lang="ts">
// Alles ist explizit und typsicher
// Props - volle Typisierung
const props = defineProps<{ user: User }>()
// Emits - Payload-Typen
const emit = defineEmits<{ update: [value: string] }>()
// State - Inference oder explizit
const count = ref(0)
const user = ref<User | null>(null)
// Composables - generische Rückgabetypen
const { data, isLoading } = useFetch<User[]>('/api/users')
// Computed - automatische Typisierung
const userName = computed(() => user.value?.name ?? 'Gast')
</script>

Keine Wrapper, kein this, volle IDE-Unterstützung, Generics funktionieren

TypeScript bietet nützliche eingebaute Utility-Types:

interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
// Alle Properties optional
type PartialUser = Partial<User>
// Alle Properties required
type RequiredUser = Required<User>
// Nur bestimmte Properties
type UserCredentials = Pick<User, 'email' | 'role'>
// Properties ausschließen
type PublicUser = Omit<User, 'email'>
// Readonly
type ReadonlyUser = Readonly<User>
// Record für Key-Value Maps
type UserRoles = Record<string, 'admin' | 'user'>
// Fehler: Object is possibly null
user.value.name
// Optional Chaining
user.value?.name
// Non-null Assertion (wenn sicher)
user.value!.name
// Type Guard
if (user.value) {
user.value.name // OK
}
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"jsxImportSource": "vue",
"noEmit": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

Vue + TypeScript:

TypeScript Grundlagen: