Ошибка "Property does not exist on type 'never'" в TypeScript при работе с Vue.js
- {{ item.name }}
Причина ошибки
Ошибка "Property does not exist on type 'never'" возникает из-за того, что TypeScript не может вывести тип элементов массива, когда массив объявлен пустым (ref([])) или когда пропс объявлен без явной типизации. В таких случаях TypeScript выводит тип массива как never[], а элемент соответственно как never — тип, который не имеет ни одного значения и, следовательно, никаких свойств.
Решение: правильная типизация
Нужно явно указать тип для массива на всех уровнях: при создании реактивной переменной, при объявлении пропсов и, опционально, в шаблоне.
Шаг 1: Типизируйте ref
// ParentComponent.vue
import { ref } from 'vue'
interface Item {
id: number
name: string
}
const items = ref<Item[]>([]) // Явно указываем тип массива
Теперь TypeScript знает, что items — это массив объектов с полями id и name.
Шаг 2: Типизируйте пропсы в дочернем компоненте
Лучший способ — использовать defineProps с дженериком:
// ChildComponent.vue
<script setup lang="ts">
interface Item {
id: number
name: string
}
const props = defineProps<{
items: Item[]
}>()
</script>
Если вы предпочитаете старый синтаксис с объектом, укажите тип через as PropType:
import { PropType } from 'vue'
defineProps({
items: {
type: Array as PropType<Item[]>,
required: true
}
})
Шаг 3: (Опционально) Используйте дженерик для v-for
В некоторых сложных случаях может потребоваться явно указать тип в шаблоне:
<li v-for="(item: Item) in items" :key="item.id">
{{ item.name }}
</li>
Почему это работает?
- Явная типизация ref — задает тип для реактивной переменной, даже когда она пуста.
- Типизация пропсов — сообщает TypeScript, какие данные ожидает компонент.
- PropType — решает проблему, когда обычный
type: Arrayне передает TypeScript информацию о вложенных типах.
Полный исправленный код
// ParentComponent.vue
<template>
<ChildComponent :items="items" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
interface Item {
id: number
name: string
}
const items = ref<Item[]>([])
// items.value = await fetchItemsFromAPI()
</script>
// ChildComponent.vue
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<script setup lang="ts">
interface Item {
id: number
name: string
}
const props = defineProps<{
items: Item[]
}>()
</script>
<<Альтернативный подход: использование утверждения типа (type assertion)
Если вы не хотите менять объявление пропсов или ref (например, в унаследованном коде), можно использовать утверждение типа прямо в шаблоне или в вычисляемом свойстве:
// В шаблоне
<li v-for="item in (items as Array<{id: number, name: string}>)" :key="item.id">
{{ item.name }}
</li>
// Или через computed
import { computed } from 'vue'
const typedItems = computed(() => items as Array<{id: number, name: string}>)
Этот подход — "быстрый фикс", но он менее безопасен, так как обходит проверку типов. Рекомендую использовать его только для быстрого прототипирования или исправления legacy-кода. В продакшене всегда лучше явно типизировать данные.
Еще один вариант: дженерики в defineProps
В Vue 3.3+ появилась возможность использовать дженерики напрямую:
<script setup lang="ts" generic="T extends { id: number, name: string }">
defineProps<{
items: T[]
}>()
</script>
Это дает гибкость, если тип элементов может меняться, но сохраняет типизацию.