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<Theme> = 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<Ref<number>> = 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.

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

ChatGPT brings unlimited text chats to free users

Next Post

AI and ROI: Getting Your Data Ready

Related Posts