# Vue 3 z TypeScriptem w 2026: bezpieczne typowanie propsów, emitów i composables
> Praktyczny przewodnik po typowaniu komponentów Vue 3 w TypeScript: generyczne defineProps, krotkowe defineEmits, typowane composables, defineModel oraz InjectionKey wraz z pytaniami rekrutacyjnymi.
- Published: 2026-07-07
- Updated: 2026-07-07
- Author: SharpSkill
- Tags: Vue 3, TypeScript, defineProps, Composables, Frontend
- Reading time: 8 min
---
Vue 3 w połączeniu z TypeScriptem w 2026 roku zapewnia pełną, statyczną kontrolę typów obejmującą propsy, zdarzenia oraz logikę wielokrotnego użytku. Składnia `
{{ props.name }}
{{ props.role }}
```
Props `role` jest teraz ograniczony do dwóch literałów tekstowych, więc przekazanie `role="guest"` powoduje błąd kompilacji. To główny powód, by sięgać po `defineProps` z TypeScriptem: [oficjalny przewodnik Vue po TypeScript](https://vuejs.org/guide/typescript/composition-api.html) traktuje formę generyczną jako domyślną w projektach opartych na `
```
Vue 3.5 ustabilizowało **reaktywną destrukturyzację propsów**, która jest dziś zwięźlejszym wzorcem. Destrukturyzacja wyniku `defineProps` i przypisanie wartości domyślnej w tej samej instrukcji pozostaje w pełni reaktywne — kompilator pod spodem przepisuje każdy odczyt z powrotem na `props.x`.
```vue
{{ label }}
```
> **Pułapka reaktywności przy destrukturyzacji propsów**
>
> Zdestrukturyzowane propsy pozostają reaktywne w szablonie oraz w `computed`, ale przekazanie zdestrukturyzowanej wartości bezpośrednio do `watch` lub do composable odczytuje ją tylko raz i zrywa powiązanie reaktywne. Opakowuj taką wartość w getter — `watch(() => color, ...)` — albo konwertuj przez `toRef(props, 'color')`, gdy dalej potrzebny jest ref.
To częsta pułapka rekrutacyjna: kandydaci zakładają, że zdestrukturyzowana zmienna jest zwykłą wartością, podczas gdy kompilator w rzeczywistości przekierował każdy odczyt z powrotem na `props.color`.
## Bezpieczne emity dzięki defineEmits
Zdarzenia zasługują na tę samą staranność co propsy. Generyczne `defineEmits` opisuje każdą nazwę zdarzenia oraz jego ładunek w postaci krotki, dając komponentowi rodzica podpowiedzi kodu, a komponentowi dziecka kontrolę już w czasie kompilacji, że emitowane są właściwe argumenty.
```vue
```
Ta forma krotkowa zastąpiła starszą składnię sygnatur wywołania (`(e: 'search', q: string): void`), ponieważ czyta się ją lepiej i obsługuje wiele zdarzeń bez przeciążeń. Gdy zdarzenie nie niesie danych, pusta krotka `[]` dokumentuje to wprost. Połączenie typowanych emitów z typowanymi propsami daje komponenty, których cały publiczny interfejs jest weryfikowalny przed uruchomieniem — ta sama dyscyplina, którą opisuje [przewodnik po Composition API Vue](/blog/vue-nuxt/vue-3-composition-api-complete-guide).
## Typowanie composables dla logiki wielokrotnego użytku
Composables to zwykłe funkcje, więc podlegają standardowym regułom TypeScript — ale kilka konwencji utrzymuje je ergonomicznymi. Zwracaj typy `Ref` jawnie, gdy wnioskowanie nie jest oczywiste, i sięgaj po typy generyczne, gdy composable opakowuje dowolne dane, takie jak odpowiedź API.
```ts
// useFetch.ts
import { ref, type Ref } from 'vue'
interface UseFetchReturn {
data: Ref
error: Ref
loading: Ref
}
// Generic flows through to the caller's typed data
export function useFetch(url: string): UseFetchReturn {
const data = ref(null) as Ref
const error = ref(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 }
}
```
W miejscu wywołania parametr generyczny sprawia, że `data` jest w pełni otypowane bez dodatkowych adnotacji:
```ts
// UserList.vue (script setup)
interface User { id: number; name: string }
// data is Ref — inferred from the generic
const { data: users, loading } = useFetch('/api/users')
```
Jawne otypowanie zwracanego obiektu (`UseFetchReturn`) warte jest tych kilku dodatkowych linii: dokumentuje kontrakt, zapobiega przypadkowemu wyciekowi wewnętrznych refów i daje konsumentom jeden typ do zaimportowania. Głębsze wzorce composables, takie jak przeciążenia argumentów i sprzątanie powiązane z cyklem życia, omawia [zaawansowany przewodnik po composables Vue](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns).
## Typowanie defineModel dla dwukierunkowego wiązania
`defineModel`, stabilne od Vue 3.4, łączy dawny props `modelValue` oraz emit `update:modelValue` w jeden zapisywalny ref. Jego parametr generyczny typuje obie strony wiązania naraz.
```vue
```
Modele nazwane — `defineModel('title')` — mapują się na `v-model:title` i otrzymują to samo typowanie. Eliminuje to całą klasę błędów niezgodnego ładunku, które ręczny wzorzec props-plus-emit potrafił ukrywać.
## Typowanie refów szablonu i instancji komponentów
Dostęp do węzła DOM lub komponentu potomnego przez `ref` to miejsce, w którym nieotypowany kod Vue najczęściej wraca do `any`. Rozwiązaniem jest sparametryzowanie `useTemplateRef` (Vue 3.5+) lub samego `ref` typem elementu, dzięki czemu dostęp do właściwości jest sprawdzany względem rzeczywistego interfejsu DOM.
```vue
```
Dla referencji do komponentu potomnego `InstanceType` wydobywa publiczny typ komponentu, udostępniając wszystko, co dziecko zadeklarowało przez `defineExpose`. Dzięki temu wywołania metod z rodzica do dziecka są w pełni sprawdzane, a nie zgadywane.
```vue
```
## Bezpieczne provide i inject z InjectionKey
Wstrzykiwanie zależności w drzewie komponentów traci informacje o typach, o ile klucz ich nie przenosi. `InjectionKey` to otypowany symbol, który wiąże typ wartości z jej kluczem, dzięki czemu `provide` i `inject` pozostają zgodne bez ręcznego rzutowania.
```ts
// theme-key.ts
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 = Symbol('theme')
```
```ts
// 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()
```
Przekazanie wartości, której kształt nie pasuje do `ThemeContext`, jest błędem kompilacji w miejscu wstrzyknięcia, co wychwytuje rozjazd między odległymi komponentami, zanim trafi na produkcję.
## Typowe pytania rekrutacyjne o Vue i TypeScript
Rekruterzy sprawdzają, czy kandydat rozumie granicę między typami w czasie kompilacji a zachowaniem w czasie działania. Kilka powracających pytań i ich trafne odpowiedzi:
- **Dlaczego preferować `defineProps()` zamiast formy obiektowej czasu działania?** Generyczne propsy wyrażają typy unijne, sygnatury funkcji i zagnieżdżone kształty, których deklaracje czasu działania nie potrafią oddać, a przy tym usuwają zdublowane definicje typów i wartości runtime.
- **Czy zdestrukturyzowane propsy są reaktywne?** Tak, w Vue 3.5+, ponieważ kompilator przepisuje dostęp na `props.x`. Ale zdestrukturyzowane *wartości* przekazane do `watch` lub composable są odczytywane raz — użyj gettera albo `toRef`.
- **Jak otypować emitowane zdarzenie bez ładunku?** Pustą krotką: `defineEmits<{ close: [] }>()`.
- **Co zastępuje `defineModel`?** Parę: props `modelValue` oraz emit `update:modelValue`, zjednoczoną w jeden otypowany, zapisywalny ref.
Ćwiczenie tych zagadnień na prawdziwej bazie pytań wyostrza odruchy, które doceniają rozmowy rekrutacyjne — [moduł pytań rekrutacyjnych o composables Vue](/technologies/vue-nuxt/interview-questions/vue-composables) trenuje dokładnie te wzorce. Narzędzia też mają znaczenie: uruchamiaj `vue-tsc` w CI, aby błędy typów blokowały scalanie, i sięgaj po [podręcznik TypeScript](https://www.typescriptlang.org/docs/handbook/2/generics.html), gdy w grę wchodzą generyczne composables. Doświadczenie w edytorze napędza [oficjalne narzędzie Volar od Vue](https://github.com/vuejs/language-tools), które odczytuje te makra i pokazuje błędy w miejscu ich powstania.
## Podsumowanie
- Używaj generycznego `defineProps()`, aby wyrazić typy unijne, pola opcjonalne i propsy będące callbackami, których deklaracje czasu działania nie potrafią uchwycić.
- Preferuj reaktywną destrukturyzację propsów z wartościami domyślnymi w linii w Vue 3.5+, a po `withDefaults` sięgaj tylko wtedy, gdy wspólny obiekt wartości domyślnych jest czytelniejszy.
- Typuj zdarzenia formą krotkową `defineEmits`, aby ładunki były sprawdzane w czasie kompilacji zarówno w dziecku, jak i w rodzicu.
- Adnotuj typy zwracane composables jawnie i używaj typów generycznych, by przekazywać typy danych dostarczone przez wywołującego od początku do końca.
- Wybieraj `defineModel()` dla wiązania dwukierunkowego, aby zwinąć szablonowy kod props-plus-emit w jeden otypowany ref.
- Uruchamiaj `vue-tsc` w ciągłej integracji, aby regresje typów zatrzymywały build, zamiast trafiać na produkcję.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/pl/blog/vue-nuxt/vue-3-typescript-type-safe-props-emits-composables