Middle: питання на співбесіді з теми «TypeScript та інструменти»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
defineEmits оголошує, які події компонент може випромінювати і з якими аргументами.
Сучасний синтаксис (Vue 3.3+) - іменовані кортежі:
<script setup lang="ts">
const emit = defineEmits<{
change: [id: number]
update: [value: string, source: 'keyboard' | 'paste']
close: []
}>()
emit('change', 42)
emit('update', 'текст', 'paste')
emit('change', 'abc') // помилка TypeScript
emit('unknown') // помилка TypeScript
</script>
Старіший синтаксис - сигнатури викликів, працює так само:
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'close'): void
}>()
Батьківський компонент отримує перевірку типів у шаблоні: @change="(id) => select(id)" - id має тип number, а підписка на неоголошену подію підсвічується редактором (з розширенням Vue - Official і vue-tsc).
Навіщо оголошувати події взагалі:
- документація компонента: з першого рядка видно його «вихідний» інтерфейс;
- атрибути-слухачі не протікають на корінь: оголошена подія не потрапляє у
$attrs. Неоголошена - потрапляє і може навісити нативний обробник на кореневий елемент, через що@clickспрацює двічі; - перевірка в режимі розробки: з runtime-валідацією (об'єктний синтаксис з функціями-валідаторами) Vue попередить про некоректне корисне навантаження.
Типові помилки:
- події в стилі camelCase і kebab-case: у шаблоні батька пишуть
@item-selected, у дочірньомуemit('itemSelected')- Vue їх зіставляє, але в типах і пошуку по коду краще одна форма; - зміна props замість події: дочірній компонент мутує об'єкт із props («він же не примітив, працює») - потік даних стає непрозорим. Зміну даних батька - лише через подію чи
defineModel; - занадто багато подій - знак, що компонент робить забагато або що частину стану варто винести в стор.
v-model на компоненті - це скорочення для пари «prop + подія оновлення»:
<PriceInput v-model="price" />
<!-- те саме, що -->
<PriceInput :modelValue="price" @update:modelValue="(v) => price = v" />
До Vue 3.4 дочірній компонент мусив оголосити prop, подію і зв'язати їх вручну. defineModel (3.4+) робить це одним рядком:
<!-- PriceInput.vue -->
<script setup lang="ts">
const model = defineModel<number>({ required: true })
</script>
<template>
<input type="number" v-model="model" />
</template>
model - ref: читання повертає значення від батька, присвоєння model.value = 10 випромінює update:modelValue. Компілятор створює і prop modelValue, і подію автоматично.
Кілька моделей і назви:
<!-- батько -->
<DateRange v-model:from="startDate" v-model:to="endDate" />
<!-- DateRange.vue -->
<script setup lang="ts">
const from = defineModel<string>('from')
const to = defineModel<string>('to')
</script>
Модифікатори (v-model.trim) доступні через деструктуризацію:
const [model, modifiers] = defineModel<string>({
set(value) {
return modifiers.trim ? value.trim() : value
},
})
Значення за замовчуванням і розсинхронізація. Якщо модель має default, а батько не передав їй значення (його змінна undefined), дочірній компонент працюватиме зі своїм значенням за замовчуванням: у батька undefined, у дитини, наприклад, 1. Документація прямо попереджає про цю розсинхронізацію - надійніше, щоб початкове значення задавав батько.
Пастки:
model.value.push(x)для масиву змінює об'єкт батька напряму, без події - краще присвоювати новий масив (model.value = [...model.value, x]);v-modelна<input>усередині з тим самимmodel- нормальний і найкоротший шлях обгорнути нативне поле.
ref і computed зазвичай виводять тип самі:
const count = ref(0) // Ref<number>
const doubled = computed(() => count.value * 2) // ComputedRef<number>
Явний тип потрібен, коли початкове значення не описує всі можливі:
const user = ref<User | null>(null)
const status = ref<'idle' | 'loading' | 'error'>('idle')
const total = computed<number>(() => items.value.reduce((s, i) => s + i.price, 0))
Посилання на елемент шаблону. У Vue 3.5 для цього є useTemplateRef:
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const input = useTemplateRef<HTMLInputElement>('search')
onMounted(() => input.value?.focus())
</script>
<template>
<input ref="search" />
</template>
Зв'язок іде за рядковим ключем з атрибута ref, а не за назвою змінної. Тип можна не вказувати - розширення Vue - Official виводить його з шаблону.
До 3.5 писали ref з тією самою назвою, що й атрибут:
const search = ref<HTMLInputElement | null>(null)
Посилання на дочірній компонент:
import OrderForm from './OrderForm.vue'
const form = useTemplateRef<InstanceType<typeof OrderForm>>('form')
form.value?.reset() // доступно, лише якщо OrderForm зробив defineExpose({ reset })
Що варто пам'ятати:
- значення ref шаблону
nullдо монтування і після розмонтування - тому тип міститьnull, а звертання йде через?.чи вonMounted; - елемент під
v-ifможе зникнути - посилання станеnull; - у
v-forref стає масивом елементів, і його порядок не гарантовано збігається з порядком у масиві даних; shallowRefдоречний для великих об'єктів, які замінюють цілком, - тип той самий, але вміст не стає глибоко реактивним.
Докладніше в документації: TypeScript: типізація ref шаблону
Компілятор TypeScript (tsc) працює з .ts і .tsx. Файл .vue для нього - невідомий формат: шаблон, <script setup> з макросами, стилі в одному файлі. Тому для Vue є окремий інструментарій на основі Volar.
Що перевіряє типи:
vue-tsc- обгортка надtsc, що «розуміє» SFC: перевіряє і скрипт, і вирази в шаблоні (props, події, слоти,v-for);- розширення Vue - Official (колишній Volar) для VS Code - те саме в редакторі, плюс автодоповнення в шаблонах. Для інших редакторів - мовний сервер
@vue/language-server.
Чому окремий крок. Vite (і esbuild/Rolldown під ним) лише прибирає типи з коду - не перевіряє. Помилка типу не зупинить npm run dev. Тому перевірку запускають окремо:
"scripts": {
"type-check": "vue-tsc --build",
"build": "vue-tsc --build && vite build"
}
і обов'язково в CI - інакше помилки типів накопичуються непомітно.
Що потрібно в налаштуваннях:
tsconfigз"jsx": "preserve"і модулями для збирача ("moduleResolution": "bundler") -create-vueгенерує це через пакет@vue/tsconfig;- оголошення для імпорту
.vue-файлів у звичайних.tsне потрібне, якщо використовуєтьсяvue-tsc; старийshims-vue.d.tsзdeclare module '*.vue'робить усі компонентиanyі вимикає перевірку props; - типи середовища Vite (
/// <reference types="vite/client" />) дляimport.meta.envта імпорту ассетів.
Перевірка шаблонів - головна цінність:
<OrderCard :order="order" @cancel="onCancel" />
<!-- vue-tsc: Property 'total' is missing / Argument of type 'string' is not assignable to 'number' -->
Помилки на межі компонентів (неправильний prop, неіснуюча подія, слот з іншими параметрами) інакше виявляються лише під час виконання.
Строгість шаблонів регулюється опціями vueCompilerOptions у tsconfig (наприклад, strictTemplates) - у великих проєктах їх вмикають поступово.