Vue 3 裡被嚴重低估的 API:InjectionKey

一、先說個場景

Hello~大家好,我是秋天的一陣風

寫 TypeScript 的你,provide/inject 是不是還在裸奔?

你肯定寫過這種程式碼:

ts 程式碼解讀複製程式碼// 祖先元件
provide('theme', ref('dark'))

// 後代元件
const theme = inject('theme') // theme 的型別是 any,你心裡沒點數嗎

跑起來沒問題,但你把 'theme' 打成 'thme',TypeScript 一聲不吭。等到線上樣式崩了你才發現——拼字錯誤,堪稱前端工程師的經典翻車現場。

更窒息的是,theme 推導出來的型別是 any。你想用 .value?隨便用。你想呼叫 .toUpperCase()?也隨便用。反正 TypeScript 不攔你,報不報錯全靠運氣。

這就是 InjectionKey 要解決的問題。

二、InjectionKey 到底是啥

一句話:給 provide/inject 加型別標註的鑰匙。

它的型別定義長這樣(原始碼在這):

ts 程式碼解讀複製程式碼interface InjectionConstraint<T> {}

export type InjectionKey<T> = symbol & InjectionConstraint<T>

拆開來看:

  1. InjectionConstraint<T> 是一個空介面,裡面什麼都沒有。
  2. InjectionKey<T>symbol 和這個空介面的交叉型別

一個空介面有什麼用?它在執行時什麼都不做,編譯成 JS 之後直接消失。但 TypeScript 編譯器會「記住」這個泛型參數 T

ts 程式碼解讀複製程式碼const key: InjectionKey<string> = Symbol('test')

console.log(typeof key) // 'symbol',它就是個普通 Symbol

執行時的 key 就是一個乾乾淨淨的 Symbol,沒有魔法。

那它圖什麼?圖的是型別層面的資訊傳遞

TypeScript 編譯器看到你寫了 InjectionKey<Ref<string>>,它就「記住」了:這個 Symbol 對應的值應該是 Ref<string>。等你用 inject(key) 的時候,編譯器根據這個記憶自動推導回傳型別。

ts 程式碼解讀複製程式碼const themeKey: InjectionKey<Ref<string>> = Symbol('theme')

// TypeScript 編譯器的內心獨白:
// "themeKey 是 InjectionKey<Ref<string>>,
//  所以 inject(themeKey) 的返回值應該是 Ref<string> | undefined"

這其實是 TypeScript 的一個經典技巧,叫品牌型別(branded type)。用來給同一種底層型別打上不同的「品牌」:

ts 程式碼解讀複製程式碼type UserId = string & { __brand: 'userId' }
type OrderId = string & { __brand: 'orderId' }

const uid: UserId = '123' as UserId
const oid: OrderId = '456' as OrderId

// 雖然執行時都是 string,但 TypeScript 認為它們是不同型別
// 你不能把 UserId 賦值給 OrderId,編譯報錯

InjectionKey 用的是同一招:把不同的泛型參數 T 當作不同的「品牌」,這樣每個 InjectionKey 的型別資訊就不會混在一起。

打個比方:想像你有一把鑰匙,鑰匙上貼了個小紙條寫著「開臥室門」。鑰匙本身是金屬做的,紙條不影響開鎖功能。但你一看紙條就知道這把鑰匙對應哪扇門。symbol 就是鑰匙本身,InjectionConstraint<T> 就是那張紙條——紙條不參與開鎖,但它幫你快速找到正確的鑰匙。

總結就是:InjectionConstraint<T> 執行時是空氣,編譯時是 TypeScript 推導型別的依據。

三、怎麼用

第一步:定義一把鑰匙

ts 程式碼解讀複製程式碼// keys.ts
import type { InjectionKey, Ref } from 'vue'

export const themeKey: InjectionKey<Ref<string>> = Symbol('theme')

注意兩個細節:

  1. 用的是 Symbol('theme'),不是 'theme' 字串。Symbol 天然唯一,不存在命名衝突。
  2. 型別標註 InjectionKey<Ref<string>>,意思是:誰用這把鑰匙 inject,拿到的一定是 Ref<string>

第二步:provide 的時候用它

ts 程式碼解讀複製程式碼// 祖先元件
import { provide, ref } from 'vue'
import { themeKey } from './keys'

const theme = ref('dark')
provide(themeKey, theme)

如果你 provide 的值和鑰匙的型別對不上,TypeScript 直接報錯:

ts 程式碼解讀複製程式碼provide(themeKey, 42) // ❌ 型別 'number' 不能賦值給型別 'Ref<string>'

編譯期就攔住你,不用等到執行時炸掉。

第三步:inject 的時候也用它

ts 程式碼解讀複製程式碼// 後代元件
import { inject } from 'vue'
import { themeKey } from './keys'

const theme = inject(themeKey) // 型別自動推導為 Ref<string> | undefined

看見沒?不用手動寫型別了。inject(themeKey) 自動知道回傳的是 Ref<string>(或者 undefined,因為可能沒有人 provide)。

如果你想斷言它一定存在:

ts 程式碼解讀複製程式碼const theme = inject(themeKey)! // Ref<string>
// 或者給個預設值
const theme = inject(themeKey, ref('light')) // Ref<string>

四、實戰:全域狀態管理

光說理論不過癮,來個實際場景。假設你在做一個多主題切換的專案:

ts 程式碼解讀複製程式碼// keys.ts
import type { InjectionKey, Ref, ComputedRef } from 'vue'

interface ThemeContext {
  current: Ref<string>
  toggle: () => void
  isDark: ComputedRef<boolean>
}

export const themeKey: InjectionKey<ThemeContext> = Symbol('theme')
ts 程式碼解讀複製程式碼// ThemeProvider.vue
<script setup lang="ts">
import { ref, computed, provide } from 'vue'
import { themeKey } from './keys'

const current = ref('dark')
const toggle = () => {
  current.value = current.value === 'dark' ? 'light' : 'dark'
}
const isDark = computed(() => current.value === 'dark')

provide(themeKey, { current, toggle, isDark })
</script>

<template>
  <slot />
</template>
ts 程式碼解讀複製程式碼// 任意後代元件
<script setup lang="ts">
import { inject } from 'vue'
import { themeKey } from './keys'

const theme = inject(themeKey)!

// 全部都有型別提示,一個字母都不會錯
theme.current.value  // string
theme.toggle()       // void
theme.isDark.value   // boolean
</script>

五、和 defineProps 的對比

你可能會問:這和 defineProps 的型別標註思路不是一樣嗎?

沒錯,核心思想一樣——用型別系統約束執行時行為。差別在於:

definePropsInjectionKey作用域父 → 子(元件樹)祖先 → 任意後代(跨層級)型別標註方式泛型參數單獨定義 Symbol編譯時檢查✅✅執行時驗證可選(withDefaults)無defineProps 是垂直方向的型別安全,InjectionKey 是穿透方向的型別安全。兩者不衝突,各管各的。

六、幾個容易踩的坑

坑一:鑰匙放哪

別把鑰匙定義在元件檔裡,否則每次 import 可能拿到不同的 Symbol 實例。統一放在一個 keys.ts 檔案裡:

css 程式碼解讀複製程式碼src/
├── keys.ts          ← 所有 InjectionKey 集中管理
├── components/
│   └── ...

坑二:別忘了 undefined

inject() 的回傳型別永遠包含 undefined,因為沒有人能保證上游一定 provide 了。要嘛用 ! 斷言,要嘛給預設值,要嘛老老實實做空值判斷。

坑三:響應式丟失

provide 一個普通物件,下游拿到的是非響應式的。要保證響應式,要嘛傳 ref / reactive,要嘛傳整個 composables 的回傳值(上面 ThemeContext 的例子就是這麼做的)。

總結

InjectionKey 解決的問題很簡單:讓 provide/inject 從「口頭約定」變成「合約約束」。

沒有它,你靠字串匹配,靠自覺,靠祈禱。 有了它,TypeScript 幫你盯著,拼錯一個字母編譯就過不了。

就這一個 Symbol 的包裝,值得你在每個用了 provide/inject 的專案裡都加上。

寫 Vue 3 + TypeScript 不用 InjectionKey,就像寫合約不蓋章——雙方口頭答應了,出事了誰也不認。


原文出處:https://juejin.cn/post/7654840513968537641


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝14   💬2   ❤️1
662
🥈
我愛JS
📝1   ❤️1
71
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登