Typing Vue 3 provide/inject Without Losing Autocomplete
Strict prop types and typed emits get most of the attention in Vue 3 + TypeScript setups, but provide/inject is where type safety quietly falls apart if you use the API the way the docs show it by default. inject() without a type hint returns unknown, which means every consumer of an injected value either casts it blindly or loses autocomplete entirely — and a typo in the injection key becomes a runtime undefined instead of a compile-time error. The Default Setup Is Untyped by Construction The naive version compiles, but gives you nothing: // Provider provide('theme', currentTheme); // Consumer const theme = inject('theme'); // type: unknown Nothing here catches a typo in the key string, and nothing tells the consumer what shape theme actually has. Both problems come from using a plain string as the injection key. InjectionKey Fixes Both Problems at Once Vue exports an InjectionKey type specifically for this. Define it once, typed, and both provide and inject become fully type-checked against the same symbol: // keys.ts import type { InjectionKey } from 'vue'; export interface Theme { mode: 'light' | 'dark'; accentColor: string; } export const ThemeKey: InjectionKey = Symbol('theme'); // Provider import { ThemeKey } from './keys'; provide(ThemeKey, { mode: 'dark', accentColor: '#4f46e5' }); // Consumer import { ThemeKey } from './keys'; const theme = inject(ThemeKey); // type: Theme | undefined The | undefined in that last type isn't a quirk — it's inject being honest that a consumer might render without a matching provider above it in the tree, which is a real runtime possibility TypeScript is right to force you to handle. Handling the undefined Case Without Littering ?. Everywhere The common mistake is providing a default value to silence the undefined type instead of actually checking for it: const theme = inject(ThemeKey, { mode: 'light', accentColor: '#000' }); // default masks missing provider This works but hides a real bug — a component rendered outside its expected provider tree — behind a silent fallback. For anything where the missing-provider case matters (which is most cases beyond pure UI theming), throw instead: export function useTheme(): Theme { const theme = inject(ThemeKey); if (!theme) { throw new Error('useTheme() called without a ThemeKey provider in the component tree'); } return theme; } Wrapping every injection in a small composable like useTheme() rather than calling inject(ThemeKey) directly at each call site means the error message, the null check, and the type narrowing all live in exactly one place instead of being copy-pasted at every consumer. Typing Injected Reactive State Correctly If the provided value is a ref or reactive object, the InjectionKey generic should reflect that, not the unwrapped shape, or you'll fight the type checker over .value access: import type { InjectionKey, Ref } from 'vue'; export const CounterKey: InjectionKey = Symbol('counter'); provide(CounterKey, ref(0)); const count = inject(CounterKey); // Ref | undefined, .value works correctly Getting this generic wrong — typing the key as InjectionKey when you're actually providing ref(0) — is a common source of confusing type errors where .value seems to not exist on a type that should have it. Where This Fits Into the Rest of Your Type Setup provide/inject is one piece of a larger strict-typing setup — it needs to sit alongside typed props, typed emits, and a properly configured tsconfig.json to actually pay off. If you haven't set up the rest of that foundation yet, this walkthrough of strict types for Vue 3 components covers defineProps, defineEmits, composable typing, and the gradual-adoption path for turning on strict mode in an existing codebase without a full rewrite.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to