provide/inject передає значення від предка до будь-якого нащадка без прокидання через props. З рядковим ключем TypeScript не знає, що там лежить:
provide('theme', theme)
const theme = inject('theme') // unknown - і помилка в назві ключа ніде не підсвітиться
InjectionKey<T> - символ, що несе тип значення:
// keys.ts
import type { InjectionKey, Ref } from 'vue'
export interface ThemeContext {
mode: Ref<'light' | 'dark'>
toggle: () => void
}
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
// провайдер
provide(ThemeKey, { mode, toggle }) // TypeScript перевірить форму значення
// споживач
const theme = inject(ThemeKey) // ThemeContext | undefined
Чому undefined у типі: компонент може опинитися поза деревом провайдера - тоді inject поверне undefined (з попередженням у режимі розробки). Варіанти:
const theme = inject(ThemeKey, defaultTheme) // значення за замовчуванням
function useTheme(): ThemeContext {
const theme = inject(ThemeKey)
if (!theme) throw new Error('useTheme() потрібно викликати всередині ThemeProvider')
return theme
}
Обгортка-composable з явною помилкою - найпрактичніший варіант: зрозуміле повідомлення замість Cannot read properties of undefined десь глибоко.
Чому символ кращий за рядок:
- немає зіткнень: дві бібліотеки з ключем
'theme'перезапишуть одна одну; символи завжди унікальні; - тип прив'язаний до ключа - не треба дублювати тип у кожному
inject; - рефакторинг і пошук працюють за імпортом ключа, а не за рядком.
Що надавати:
- реактивні значення (
ref,computed) - інакше нащадки не побачать змін; - функції для зміни замість відкритого змінюваного стану - провайдер контролює, як змінюється дані. Для захисту від прямої зміни -
readonly(state).
Коли не provide/inject: глобальний стан застосунку (кошик, користувач) - Pinia з DevTools і плагінами. Provide/inject - для контексту піддерева: форма й її поля, таблиця й колонки, тема окремого віджета.
Докладніше в документації: TypeScript: типізація provide / inject