Vue 3 Script Setup та defineModel у 2026: Сучасний Синтаксис та Питання на Співбесідах
Вичерпний посібник з синтаксису script setup у Vue 3 та макросу defineModel. Огляд reactive props destructure, типізації TypeScript та питань на співбесідах.

Синтаксис script setup у Vue 3 трансформує підхід до написання компонентів, усуваючи шаблонний код та значно покращуючи інтеграцію з TypeScript. У поєднанні з defineModel, введеним експериментально у Vue 3.3 та стабілізованим у Vue 3.4, створення компонентів з двостороннім зв'язуванням даних потребує значно менше коду порівняно з еквівалентом в Options API.
Починаючи з Vue 3.5, властивості (props) можна деструктуризувати безпосередньо з defineProps, зберігаючи реактивність. У більшості випадків це усуває необхідність використання toRefs.
Основи Script Setup у Vue 3.5
Синтаксис <script setup> — це синтаксичний цукор часу компіляції для Composition API всередині однофайлових компонентів (Single File Components). Змінні, оголошені на верхньому рівні, автоматично доступні в шаблоні, а імпорти можна використовувати напряму без реєстрації.
<!-- UserCard.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { User } from '@/types'
// Props з TypeScript інтерфейсом
const props = defineProps<{
user: User
showEmail?: boolean
}>()
// Реактивний стан
const isExpanded = ref(false)
// Computed властивість
const displayName = computed(() => {
return `${props.user.firstName} ${props.user.lastName}`
})
// Емітер подій з типізованим payload
const emit = defineEmits<{
select: [userId: string]
delete: [userId: string]
}>()
// Методи — це звичайні функції
function handleSelect() {
emit('select', props.user.id)
}
</script>
<template>
<div class="user-card" @click="handleSelect">
<h3>{{ displayName }}</h3>
<p v-if="showEmail">{{ user.email }}</p>
<button @click.stop="isExpanded = !isExpanded">
{{ isExpanded ? 'Згорнути' : 'Розгорнути' }}
</button>
</div>
</template>Компілятор автоматично обробляє інструкції експорту та реєстрацію компонентів. Це зменшує когнітивне навантаження порівняно з явним поверненням значень з функції setup().
Деструктуризація Реактивних Props (Vue 3.5+)
До Vue 3.5 деструктуризація props порушувала реактивність, оскільки деструктуризація в JavaScript створює знімок значення на момент виконання. Vue 3.5 впроваджує відстеження на рівні компілятора, яке зберігає реактивність для деструктуризованих props.
<!-- SearchInput.vue -->
<script setup lang="ts">
import { watch } from 'vue'
// Деструктуризація з значеннями за замовчуванням — залишається реактивною в 3.5+
const { query = '', placeholder = 'Пошук...' } = defineProps<{
query?: string
placeholder?: string
}>()
// Watch працює на деструктуризованих props
watch(
() => query,
(newQuery) => {
console.log('Запит змінився:', newQuery)
}
)
</script>
<template>
<input
:value="query"
:placeholder="placeholder"
@input="$emit('update:query', ($event.target as HTMLInputElement).value)"
/>
</template>Цей синтаксис потребує Vue 3.5 або новішої версії. Документація Vue 3.5 release announcement описує повну поведінку деструктуризації реактивних props.
defineModel для Двостороннього Зв'язування
До defineModel реалізація v-model на власному компоненті вимагала оголошення prop та ручного випромінювання події оновлення. Цей патерн зустрічається в безлічі Vue кодових баз:
<!-- Старий спосіб — багатослівний, але явний -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
function updateValue(event: Event) {
emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>З defineModel той самий компонент стає значно коротшим:
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel створює ref, синхронізований з v-model
const model = defineModel<string>({ default: '' })
</script>
<template>
<input
v-model="model"
type="text"
class="text-input"
/>
</template>Батьківський компонент використовує стандартний синтаксис v-model:
<!-- ParentForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import TextInput from './TextInput.vue'
const username = ref('')
</script>
<template>
<TextInput v-model="username" />
<p>Ім'я користувача: {{ username }}</p>
</template>defineModel повертає ref, який залишається синхронізованим зі зв'язаним значенням батьківського компонента. Мутації цього ref автоматично випромінюють подію оновлення.
Готовий до співбесід з Vue.js / Nuxt.js?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Іменовані Моделі та Множинні Зв'язування v-model
Компоненти іноді потребують кількох двосторонніх зв'язувань. Vue підтримує іменовані зв'язування v-model, і defineModel обробляє їх, приймаючи аргумент імені.
<!-- DateRangePicker.vue -->
<script setup lang="ts">
// Іменовані моделі для множинних зв'язувань v-model
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })
// Валідація через computed
const isValidRange = computed(() => {
if (!startDate.value || !endDate.value) return true
return startDate.value <= endDate.value
})
</script>
<template>
<div class="date-range-picker">
<input
type="date"
:value="startDate?.toISOString().split('T')[0]"
@input="startDate = new Date(($event.target as HTMLInputElement).value)"
/>
<span>до</span>
<input
type="date"
:value="endDate?.toISOString().split('T')[0]"
@input="endDate = new Date(($event.target as HTMLInputElement).value)"
/>
<p v-if="!isValidRange" class="error">Кінцева дата повинна бути пізніше початкової</p>
</div>
</template>Батьківський компонент зв'язує обидві моделі:
<DateRangePicker v-model:start="filterStart" v-model:end="filterEnd" />Модифікатори v-model з defineModel
Вбудовані модифікатори Vue, такі як .trim, .number та .lazy, працюють автоматично з defineModel. Користувацькі модифікатори потребують явної обробки через другий елемент поверненого кортежу.
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Доступ до модифікаторів через друге повернене значення
const [model, modifiers] = defineModel<number>({
default: 0,
// Функції трансформації для користувацьких модифікаторів
set(value) {
// Округлити до 2 десяткових знаків, якщо присутній модифікатор 'round'
if (modifiers.round) {
return Math.round(value * 100) / 100
}
return value
}
})
</script>
<template>
<div class="currency-input">
<span>₴</span>
<input
type="number"
step="0.01"
:value="model"
@input="model = parseFloat(($event.target as HTMLInputElement).value) || 0"
/>
</div>
</template>Використання з користувацьким модифікатором:
<CurrencyInput v-model.round="price" />Патерни TypeScript для Script Setup
Безпека типів у компонентах <script setup> базується на дженериках, що передаються до макросів компілятора. Патерн дещо відрізняється між props, emits та моделями.
<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: string }">
import { computed } from 'vue'
// Props дженерик-компонента
const props = defineProps<{
items: T[]
selected?: T
}>()
const emit = defineEmits<{
select: [item: T]
remove: [id: string]
}>()
// Слоти також можна типізувати
defineSlots<{
default: (props: { item: T; index: number }) => any
empty: () => any
}>()
const hasItems = computed(() => props.items.length > 0)
</script>
<template>
<ul v-if="hasItems">
<li
v-for="(item, index) in items"
:key="item.id"
:class="{ selected: selected?.id === item.id }"
@click="emit('select', item)"
>
<slot :item="item" :index="index">
{{ item.id }}
</slot>
</li>
</ul>
<div v-else>
<slot name="empty">Немає елементів</slot>
</div>
</template>Дженерик-компоненти, доступні з Vue 3.3, дозволяють створювати повторно використовувані типізовані компоненти без втрати виведення типів у коді, що їх використовує. Для глибших патернів документація Vue про TypeScript охоплює граничні випадки.
Типові Питання на Співбесідах про Script Setup
Інтерв'юери, що тестують знання Vue, часто фокусуються на відмінностях між script setup та Options API, а також на механіці реактивності.
Питання: Чому деструктуризація props порушує реактивність і як Vue 3.5 це вирішує?
У JavaScript деструктуризація створює нові змінні, що містять значення на момент деструктуризації. Якщо props.count зміниться пізніше, деструктуризована const { count } = props все ще міститиме старе значення. Компілятор Vue 3.5 трансформує деструктуризовані змінні в геттери, які перевираховуються при зверненні, зберігаючи реактивність без накладних витрат під час виконання.
Питання: Що відбувається під капотом, коли використовується defineModel?
Компілятор розгортає defineModel в оголошення prop та реєстрацію emit. Під час виконання він створює ref, геттер якого читає значення prop, а сеттер викликає emit('update:modelValue', newValue). Цей ref не є справжньою копією prop, а проксі, яке відповідно перенаправляє читання та запис.
Питання: Коли компонент повинен використовувати script setup замість звичайної функції setup?
Script setup є кращим вибором для більшості однофайлових компонентів, оскільки він зменшує шаблонний код та покращує підтримку IDE. Звичайна функція setup() необхідна, коли компонент повинен експонувати методи батьківським компонентам через defineExpose у складних патернах, або коли створюються renderless-компоненти, що повертають функції рендерингу напряму. Для підготовки до співбесіди варто дослідити більше патернів Vue composables.
Міграція з Options API до Script Setup
Існуючі кодові бази часто містять компоненти Options API, які можуть виграти від міграції. Конвертація слідує передбачуваному патерну:
<!-- До: Options API -->
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
props: {
items: { type: Array as PropType<string[]>, required: true }
},
emits: ['select'],
data() {
return { searchTerm: '' }
},
computed: {
filteredItems() {
return this.items.filter(item =>
item.toLowerCase().includes(this.searchTerm.toLowerCase())
)
}
},
methods: {
selectItem(item: string) {
this.$emit('select', item)
}
}
})
</script><!-- Після: Script Setup -->
<script setup lang="ts">
import { ref, computed } from 'vue'
const props = defineProps<{ items: string[] }>()
const emit = defineEmits<{ select: [item: string] }>()
const searchTerm = ref('')
const filteredItems = computed(() =>
props.items.filter(item =>
item.toLowerCase().includes(searchTerm.value.toLowerCase())
)
)
function selectItem(item: string) {
emit('select', item)
}
</script>Мігрована версія коротша, і виведення типів працює без перетворень PropType. Команда Vue надає vue-codemod для автоматичних міграцій, хоча ручний огляд залишається необхідним для складних компонентів.
Міркування щодо Продуктивності в Script Setup
Компоненти script setup компілюються в більш ефективний код, оскільки компілятор знає точну форму компонента під час збірки. Зокрема:
- Зв'язування шаблону вирішуються під час компіляції, а не через runtime property lookup
- Невикористані імпорти ефективніше видаляються через tree-shaking
- Відсутність функції-обгортки зменшує накладні витрати на closure
Для застосунків, що пріоритизують розмір бандла, script setup з Vapor Mode (доступний у Vue 3.6+) повністю усуває віртуальний DOM для opt-in компонентів, генеруючи натомість прямі операції DOM.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Ключові Висновки для Vue 3 Script Setup та defineModel
- Script setup зменшує шаблонний код компонента, автоматично експонуючи зв'язування верхнього рівня до шаблонів та обробляючи реєстрацію компонентів
defineModelзамінює ручний патерн prop + emit дляv-model, повертаючи ref, синхронізований зі зв'язаним значенням батьківського компонента- Vue 3.5 впроваджує деструктуризацію реактивних props, дозволяючи
const { prop } = defineProps()без втрати реактивності - Іменовані моделі підтримують множинні зв'язування
v-modelна одному компоненті, використовуючиdefineModel('name') - Дженерики TypeScript працюють з
defineProps,defineEmits,defineSlotsта дженерик-компонентами через атрибутgeneric - Міграція з Options API слідує механічному патерну: data стає refs, computed залишається computed, методи стають функціями, а компілятор обробляє експорти
Чи знайдеш ти помилку в Vue.js / Nuxt.js?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 18 вересня 2026 р.
Теги
Поділитися
Пов'язані статті

Просунуті Vue 3 Composables: патерни повторного використання та питання для співбесіди 2026
Вичерпний посібник з просунутих Vue 3 Composables: патерни повторного використання, асинхронна обробка помилок, Dependency Injection, валідація форм та актуальні питання для технічних співбесід у 2026 році.

Nuxt 4 у 2026 році: Нова структура каталогів та міграція з Nuxt 3
Повний посібник з міграції на Nuxt 4: нова структура app/, singleton-шар отримання даних, поверхнева реактивність за замовчуванням, розділення TypeScript-контексту, нормалізовані імена компонентів, Vue Router v5, зміни в управлінні head та контрольний список міграції.

Vue 3 Pinia vs Vuex: Сучасне управління станом та питання для співбесід 2026
Порівняння Pinia та Vuex: архітектура, TypeScript, Composition API, міграція, гідратація SSR та найпоширеніші питання зі співбесід щодо управління станом Vue у 2026 році.