Files
agent-skills/skills/vue-best-practices/reference/ts-provide-inject-injection-key.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

6.5 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Use InjectionKey for Type-Safe Provide/Inject MEDIUM Without InjectionKey, injected values are typed as unknown requiring manual type assertions best-practice
vue3
typescript
provide-inject
injection-key
composition-api

Use InjectionKey for Type-Safe Provide/Inject

Impact: MEDIUM - When using provide/inject with TypeScript, use InjectionKey<T> with Symbol to achieve type-safe dependency injection. Without it, injected values have unknown type or require manual type assertions.

Task Checklist

  • Define injection keys using Symbol() as InjectionKey<T>
  • Store injection keys in a shared file for provider/consumer access
  • Understand that injected values are always T | undefined
  • Provide default values to remove undefined from the type

The Problem

<!-- Provider.vue -->
<script setup lang="ts">
import { provide } from 'vue'

interface User {
  id: string
  name: string
}

const user: User = { id: '1', name: 'John' }

// String key - no type information
provide('user', user)
</script>
<!-- Consumer.vue -->
<script setup lang="ts">
import { inject } from 'vue'

// Type is unknown!
const user = inject('user')  // Type: unknown

// Must manually assert type - error prone
const user = inject('user') as User  // Works but risky
</script>

The Solution: InjectionKey

// keys.ts - Shared injection keys file
import type { InjectionKey, Ref } from 'vue'

export interface User {
  id: string
  name: string
}

export interface AuthContext {
  user: Ref<User | null>
  login: (credentials: Credentials) => Promise<void>
  logout: () => Promise<void>
}

// Type-safe injection keys
export const userKey: InjectionKey<User> = Symbol('user')
export const authKey: InjectionKey<AuthContext> = Symbol('auth')
<!-- Provider.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
import { userKey, authKey, type User, type AuthContext } from '@/keys'

const user: User = { id: '1', name: 'John' }

// Type-checked! Must provide correct type
provide(userKey, user)

// Error if type doesn't match
provide(userKey, { wrong: 'data' })  // TypeScript error!

// Complex context
const authContext: AuthContext = {
  user: ref(null),
  login: async (creds) => { /* ... */ },
  logout: async () => { /* ... */ }
}
provide(authKey, authContext)
</script>
<!-- Consumer.vue -->
<script setup lang="ts">
import { inject } from 'vue'
import { userKey, authKey } from '@/keys'

// Automatically typed as User | undefined
const user = inject(userKey)

// Type: AuthContext | undefined
const auth = inject(authKey)

// Access with proper typing
if (user) {
  console.log(user.name)  // TypeScript knows this is string
}
</script>

Handling the Undefined Type

Injected values are always potentially undefined because the provider might not exist:

<script setup lang="ts">
import { inject } from 'vue'
import { userKey, type User } from '@/keys'

// Option 1: Provide a default value (removes undefined)
const user = inject(userKey, { id: '0', name: 'Guest' })
// Type: User (not undefined)

// Option 2: Use factory function for default
const user = inject(userKey, () => ({ id: '0', name: 'Guest' }), true)
// Type: User (factory is called only if no provider)

// Option 3: Assert non-null when certain provider exists
const user = inject(userKey)!
// Type: User (use sparingly!)

// Option 4: Guard with runtime check
const user = inject(userKey)
if (!user) {
  throw new Error('User provider not found. Wrap component in UserProvider.')
}
// Type: User after check
</script>

Creating a useInject Composable

For cleaner consumer code, create typed composables:

// composables/useAuth.ts
import { inject } from 'vue'
import { authKey, type AuthContext } from '@/keys'

export function useAuth(): AuthContext {
  const auth = inject(authKey)

  if (!auth) {
    throw new Error(
      'useAuth() requires an AuthProvider ancestor. ' +
      'Make sure to wrap your component tree with <AuthProvider>.'
    )
  }

  return auth
}
<!-- Consumer.vue -->
<script setup lang="ts">
import { useAuth } from '@/composables/useAuth'

// Clean API, guaranteed non-null, proper error message
const { user, login, logout } = useAuth()
</script>

Organizing Injection Keys

For larger applications, organize keys by domain:

src/
├── injection-keys/
│   ├── index.ts        # Re-exports all keys
│   ├── auth.ts         # Auth-related keys
│   ├── theme.ts        # Theme-related keys
│   └── i18n.ts         # i18n-related keys
// injection-keys/auth.ts
import type { InjectionKey, Ref } from 'vue'

export interface AuthState {
  isAuthenticated: Ref<boolean>
  user: Ref<User | null>
}

export interface AuthActions {
  login: (email: string, password: string) => Promise<void>
  logout: () => Promise<void>
  refresh: () => Promise<void>
}

export interface AuthContext extends AuthState, AuthActions {}

export const authStateKey: InjectionKey<AuthState> = Symbol('authState')
export const authActionsKey: InjectionKey<AuthActions> = Symbol('authActions')
export const authContextKey: InjectionKey<AuthContext> = Symbol('authContext')
// injection-keys/index.ts
export * from './auth'
export * from './theme'
export * from './i18n'

Generic Injection Keys

For reusable patterns with generics:

// keys/list.ts
import type { InjectionKey, Ref } from 'vue'

export interface ListContext<T> {
  items: Ref<T[]>
  selectedItem: Ref<T | null>
  selectItem: (item: T) => void
}

// Factory function for creating typed keys
export function createListKey<T>(name: string): InjectionKey<ListContext<T>> {
  return Symbol(name) as InjectionKey<ListContext<T>>
}

// Usage
interface Product { id: string; name: string }
export const productListKey = createListKey<Product>('productList')

Best Practices Summary

  1. Always use InjectionKey for TypeScript projects
  2. Use Symbols to prevent key collisions
  3. Create composables like useAuth() for clean consumer API
  4. Throw descriptive errors when required providers are missing
  5. Organize keys in a shared location accessible to providers and consumers
  6. Provide default values when the provider is optional

Reference