Типизация пропсов и событий
defineProps с типами
Самое удобное — interface:
interface Props {
title: string;
count: number;
isActive: boolean;
items: string[];
user: User;
}
const props = defineProps<Props>();
Автодополнение и проверка работают.
Опциональные props
interface Props {
title: string;
subtitle?: string;
count?: number;
}
? — необязательный.
Дефолты через withDefaults
interface Props {
title: string;
count?: number;
items?: Item[];
user?: User | null;
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [],
user: null,
});
⚠️ Объекты и массивы — через функцию, иначе все экземпляры разделят один объект.
Тип из одного места
types/index.ts:
export interface User {
id: number;
name: string;
email: string;
}
export interface ProductCardProps {
product: Product;
showPrice?: boolean;
onAdd?: (product: Product) => void;
}
Компонент:
<script setup lang="ts">
import type { ProductCardProps } from '@/types';
const props = withDefaults(defineProps<ProductCardProps>(), {
showPrice: true,
});
</script>
Реактивная деструктуризация
С Vue 3.5+ можно деструктуризировать:
const { title, count = 0 } = defineProps<Props>();
Работает реактивно.
До 3.5 — через props.title.
Тип пропсов без interface
const props = defineProps<{
title: string;
count?: number;
items: Item[];
}>();
Работает, но переиспользовать сложнее.
Тип пропсов через type
type Props = {
title: string;
count?: number;
};
const props = defineProps<Props>();
type vs interface:
interface— можно наследоватьtype— можно union, intersection
Для props обычно interface.
defineEmits с типами
Синтаксис объекта:
type Emits = {
(e: 'save', id: number): void;
(e: 'cancel'): void;
(e: 'update', payload: { id: number; name: string }): void;
};
const emit = defineEmits<Emits>();
Сокращённый синтаксис (3.3+):
const emit = defineEmits<{
save: [id: number];
cancel: [];
update: [payload: { id: number; name: string }];
}>();
Что даёт:
- Автодополнение при
emit('...') - Проверка аргументов
- Ошибка при опечатке
Именованные параметры
const emit = defineEmits<{
update: [id: number, name: string]; // позиционные
save: [payload: { id: number; name: string }]; // именованный объект
}>();
emit('update', 5, 'Иван');
emit('save', { id: 5, name: 'Иван' });
Комбинированный пример
<script setup lang="ts">
import type { User } from '@/types';
interface Props {
user: User;
editable?: boolean;
onDelete?: (id: number) => void;
}
type Emits = {
save: [user: User];
delete: [id: number];
cancel: [];
};
const props = withDefaults(defineProps<Props>(), {
editable: false,
});
const emit = defineEmits<Emits>();
function handleSave() {
emit('save', props.user);
}
function handleDelete() {
emit('delete', props.user.id);
}
</script>
<template>
<div>
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button v-if="editable" @click="handleSave">Сохранить</button>
<button v-if="editable" @click="handleDelete">Удалить</button>
<button @click="emit('cancel')">Отмена</button>
</div>
</template>
defineModel с типами
// Простой v-model
const model = defineModel<string>();
model.value = 'new value'; // string
// С дефолтом
const model = defineModel<string>({ default: '' });
// Обязательный
const model = defineModel<string>({ required: true });
// Именованный
const count = defineModel<number>('count', { default: 0 });
Модификаторы:
const [model, modifiers] = defineModel<string>({
set(value) {
return modifiers.trim ? value.trim() : value;
},
});
Тип emit в родителе
<script setup lang="ts">
import UserCard from './UserCard.vue';
import type { User } from '@/types';
const user = ref<User>({ id: 1, name: 'Иван', email: 'ivan@example.com' });
function handleSave(u: User) {
console.log('Saved', u);
}
function handleDelete(id: number) {
console.log('Deleted', id);
}
</script>
<template>
<UserCard
:user="user"
:editable="true"
@save="handleSave"
@delete="handleDelete"
@cancel="() => {}"
/>
</template>
TS проверит типы обработчиков.
Проброс emits вверх
Если компонент-обёртка:
<script setup lang="ts">
import InnerButton from './InnerButton.vue';
defineEmits<{
click: [event: MouseEvent];
}>();
</script>
<template>
<InnerButton @click="$emit('click', $event)" />
</template>
Или через defineOptions({ inheritAttrs: false }) + v-bind="$attrs".
Тип слота
defineSlots<{
default: (props: { item: Item }) => any;
header: () => any;
footer: (props: { total: number }) => any;
}>();
Родитель:
<MyList :items="items">
<template #default="{ item }">
{{ item.name }}
</template>
</MyList>
Проблема с Boolean props
Плохо:
<MyComponent :disabled="true" />
Хорошо:
<MyComponent disabled />
Или:
<MyComponent :disabled="isDisabled" />
⚠️ :disabled="false" — не работает, используй :disabled="false" или без атрибута.
Частые ошибки
1. Забыл lang="ts" — типы игнорируются.
2. interface внутри defineProps:
// ❌ Нельзя
const props = defineProps<interface Props { title: string }>();
// ✅ Можно
interface Props { title: string; }
const props = defineProps<Props>();
3. Импорт типов через import:
// ❌ Работает не везде
import { User } from '@/types';
const props = defineProps<{ user: User }>();
// ✅ Надёжнее
import type { User } from '@/types';
4. Дефолт объекта без функции:
// ❌ Все компоненты разделят объект
withDefaults(defineProps<Props>(), { items: [] });
// ✅
withDefaults(defineProps<Props>(), { items: () => [] });
Итог
defineProps<Props>()— типизация propswithDefaults— дефолтыdefineEmits<{...}>()— сокращённый синтаксис (3.3+)defineModel<T>()— типизация v-modelimport type— для типов
Дальше — дженерик-компоненты.