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>
258 lines
6.5 KiB
Markdown
258 lines
6.5 KiB
Markdown
---
|
|
title: Use InjectionKey for Type-Safe Provide/Inject
|
|
impact: MEDIUM
|
|
impactDescription: Without InjectionKey, injected values are typed as unknown requiring manual type assertions
|
|
type: best-practice
|
|
tags: [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
|
|
|
|
```vue
|
|
<!-- 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>
|
|
```
|
|
|
|
```vue
|
|
<!-- 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
|
|
|
|
```typescript
|
|
// 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')
|
|
```
|
|
|
|
```vue
|
|
<!-- 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>
|
|
```
|
|
|
|
```vue
|
|
<!-- 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:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```typescript
|
|
// 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
|
|
}
|
|
```
|
|
|
|
```vue
|
|
<!-- 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
|
|
```
|
|
|
|
```typescript
|
|
// 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')
|
|
```
|
|
|
|
```typescript
|
|
// injection-keys/index.ts
|
|
export * from './auth'
|
|
export * from './theme'
|
|
export * from './i18n'
|
|
```
|
|
|
|
## Generic Injection Keys
|
|
|
|
For reusable patterns with generics:
|
|
|
|
```typescript
|
|
// 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
|
|
- [Vue.js TypeScript with Composition API - Provide/Inject](https://vuejs.org/guide/typescript/composition-api.html#typing-provide-inject)
|
|
- [Vue.js Provide/Inject](https://vuejs.org/guide/components/provide-inject.html)
|