# Vue 3 з TypeScript у 2026: типобезпечні props, emits та composables
> Практичний посібник із типізації компонентів Vue 3 у TypeScript: узагальнені defineProps, кортежні defineEmits, типізовані composables, defineModel та InjectionKey разом із питаннями для співбесід.
- Published: 2026-07-07
- Updated: 2026-07-07
- Author: SharpSkill
- Tags: Vue 3, TypeScript, defineProps, Composables, Frontend
- Reading time: 8 min
---
Vue 3 у поєднанні з TypeScript у 2026 році забезпечує повну статичну перевірку типів для props, подій та повторно використовуваної логіки. Синтаксис `
{{ props.name }}
{{ props.role }}
```
Props `role` тепер обмежений двома рядковими літералами, тож передавання `role="guest"` спричиняє помилку збірки. Це головна причина застосовувати `defineProps` з TypeScript: [офіційний посібник Vue з TypeScript](https://vuejs.org/guide/typescript/composition-api.html) вважає узагальнену форму типовою для проєктів на `
```
Vue 3.5 стабілізувало **реактивну деструктуризацію props**, яка сьогодні є лаконічнішим патерном. Деструктуризація результату `defineProps` і присвоєння значення за замовчуванням у тій самій інструкції залишається повністю реактивною — компілятор під капотом переписує кожне звернення назад на `props.x`.
```vue
{{ label }}
```
> **Пастка реактивності при деструктуризації props**
>
> Деструктуризовані props залишаються реактивними в шаблоні та в `computed`, але передавання деструктуризованого значення безпосередньо у `watch` чи у composable зчитує його лише раз і розриває реактивний зв’язок. Огортайте таке значення в геттер — `watch(() => color, ...)` — або перетворюйте через `toRef(props, 'color')`, коли далі потрібен ref.
Це поширена пастка на співбесідах: кандидати вважають деструктуризовану змінну звичайним значенням, тоді як компілятор насправді перенаправив кожне зчитування назад на `props.color`.
## Типобезпечні emits за допомогою defineEmits
Події заслуговують на ту саму ретельність, що й props. Узагальнений `defineEmits` описує кожну назву події та її корисне навантаження у вигляді кортежу, надаючи батьківському компоненту автодоповнення, а дочірньому — перевірку на етапі компіляції, що передаються правильні аргументи.
```vue
```
Ця кортежна форма замінила старіший синтаксис сигнатур виклику (`(e: 'search', q: string): void`), бо читається краще й підтримує кілька подій без перевантажень. Коли подія не несе даних, порожній кортеж `[]` документує це явно. Поєднання типізованих emits із типізованими props дає компоненти, весь публічний інтерфейс яких перевіряється до виконання, — та сама дисципліна, що описана в [посібнику з Composition API Vue](/blog/vue-nuxt/vue-3-composition-api-complete-guide).
## Типізація composables для повторно використовуваної логіки
Composables — це звичайні функції, тож вони підпорядковуються стандартним правилам TypeScript, але кілька домовленостей роблять їх ергономічними. Повертайте типи `Ref` явно, коли виведення неочевидне, і використовуйте узагальнення, коли composable огортає довільні дані, наприклад відповідь 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 }
}
```
У місці виклику узагальнений параметр робить `data` повністю типізованим без жодної додаткової анотації:
```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')
```
Явна анотація об’єкта, що повертається (`UseFetchReturn`), варта цих кількох додаткових рядків: вона документує контракт, запобігає випадковому витоку внутрішніх ref і дає споживачам єдиний тип для імпорту. Про глибші патерни composables, як-от перевантаження аргументів і очищення, прив’язане до життєвого циклу, читайте в [розширеному посібнику з composables Vue](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns).
## Типізація defineModel для двостороннього зв’язування
`defineModel`, стабільний із Vue 3.4, згортає колишній props `modelValue` та emit `update:modelValue` в єдиний записуваний ref. Його узагальнений параметр типізує обидва напрямки зв’язування одразу.
```vue
```
Іменовані моделі — `defineModel('title')` — відображаються на `v-model:title` й отримують ту саму типізацію. Це усуває цілий клас помилок невідповідного навантаження, які ручний патерн props-плюс-emit зазвичай приховував.
## Типізація ref шаблонів та екземплярів компонентів
Доступ до вузла DOM чи дочірнього компонента через `ref` — це місце, де нетипізований код Vue найчастіше повертається до `any`. Рішення — параметризувати `useTemplateRef` (Vue 3.5+) або сам `ref` типом елемента, щоб доступ до властивостей перевірявся щодо справжнього інтерфейсу DOM.
```vue
```
Для посилання на дочірній компонент `InstanceType` витягує публічний тип компонента, відкриваючи все, що дочірній компонент оголосив через `defineExpose`. Це тримає виклики методів від батька до дитини повністю перевіреними, а не вгаданими.
```vue
```
## Типобезпечні provide та inject з InjectionKey
Впровадження залежностей у дереві компонентів втрачає інформацію про типи, якщо ключ її не переносить. `InjectionKey` — це типізований символ, що прив’язує тип значення до його ключа, тож `provide` та `inject` залишаються узгодженими без ручного приведення.
```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()
```
Надання значення, форма якого не відповідає `ThemeContext`, є помилкою компіляції в місці впровадження, що вловлює розбіжність між віддаленими компонентами ще до потрапляння в продакшн.
## Типові питання для співбесід із Vue та TypeScript
Співрозмовники перевіряють, чи розуміє кандидат межу між типами часу компіляції та поведінкою під час виконання. Кілька повторюваних питань і чіткі відповіді на них:
- **Чому варто надавати перевагу `defineProps()` перед об’єктною формою часу виконання?** Узагальнені props виражають об’єднані типи, сигнатури функцій та вкладені форми, яких оголошення часу виконання не можуть відтворити, і водночас усувають дубльовані визначення типів і часу виконання.
- **Чи є деструктуризовані props реактивними?** Так, у Vue 3.5+, бо компілятор переписує доступ на `props.x`. Але деструктуризовані *значення*, передані у `watch` чи composable, зчитуються один раз — використовуйте геттер або `toRef`.
- **Як типізувати подію без корисного навантаження?** Порожнім кортежем: `defineEmits<{ close: [] }>()`.
- **Що замінює `defineModel`?** Пару: props `modelValue` та emit `update:modelValue`, об’єднану в один типізований записуваний ref.
Відпрацювання цих тем на справжній базі питань загострює рефлекси, які цінують співбесіди, — [модуль питань для співбесід із composables Vue](/technologies/vue-nuxt/interview-questions/vue-composables) тренує саме ці патерни. Інструменти теж важливі: запускайте `vue-tsc` у CI, щоб помилки типів блокували злиття, і звертайтеся до [довідника TypeScript](https://www.typescriptlang.org/docs/handbook/2/generics.html), коли до справи долучаються узагальнення composables. Досвід у редакторі забезпечує [офіційний інструментарій Volar від Vue](https://github.com/vuejs/language-tools), який читає ці макроси й показує помилки прямо в коді.
## Висновок
- Використовуйте узагальнений `defineProps()`, щоб виразити об’єднані типи, необов’язкові поля та props-колбеки, яких оголошення часу виконання не здатні охопити.
- Надавайте перевагу реактивній деструктуризації props із вбудованими значеннями за замовчуванням у Vue 3.5+, а до `withDefaults` вдавайтеся лише тоді, коли спільний об’єкт значень за замовчуванням зрозуміліший.
- Типізуйте події кортежною формою `defineEmits`, щоб навантаження перевірялися на етапі компіляції як у дитині, так і в батькові.
- Явно анотуйте типи, що повертають composables, і використовуйте узагальнення, щоб наскрізно передавати надані викликачем типи даних.
- Застосовуйте `defineModel()` для двостороннього зв’язування, щоб згорнути шаблонний код props-плюс-emit в один типізований ref.
- Запускайте `vue-tsc` у безперервній інтеграції, щоб регресії типів зупиняли збірку, а не потрапляли в продакшн.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/uk/blog/vue-nuxt/vue-3-typescript-type-safe-props-emits-composables