]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(reactivity): normalize toRef property keys before dep lookup + improve types...
authoredison <daiwei521@126.com>
Wed, 25 Mar 2026 06:30:10 +0000 (14:30 +0800)
committerGitHub <noreply@github.com>
Wed, 25 Mar 2026 06:30:10 +0000 (14:30 +0800)
close #12427
close #12431

packages-private/dts-test/ref.test-d.ts
packages/reactivity/__tests__/ref.spec.ts
packages/reactivity/src/ref.ts

index cf99b7bca7a73493835d5edc4bd2d72f8a713e70..5d50146472952f4275a98d35445e1aa7e7f85542 100644 (file)
@@ -338,6 +338,14 @@ expectType<{ name: string } | null>(p2.union)
   // Should not distribute Refs over union
   expectType<Ref<number | string>>(toRef(obj, 'c'))
 
+  const array = reactive(['a', 'b'])
+  expectType<Ref<string>>(toRef(array, '1'))
+  expectType<Ref<string>>(toRef(array, '1', 'fallback'))
+
+  const tuple: [string, number] = ['a', 1]
+  expectType<Ref<string>>(toRef(tuple, '0'))
+  expectType<Ref<number>>(toRef(tuple, '1'))
+
   expectType<Ref<number>>(toRef(() => 123))
   expectType<Ref<number | string>>(toRef(() => obj.c))
 
index 25fe87aca93691c6375ef84944ffec2c930a22ed..87595dbc73a6b7cc7c3a167da4e904e066a5956b 100644 (file)
@@ -388,6 +388,37 @@ describe('reactivity/ref', () => {
     expect(bar.value).toBe(6)
   })
 
+  test('triggerRef on toRef created from array coerces property keys', () => {
+    const assertTriggerRef = (key: unknown) => {
+      const array = reactive(['a'])
+      const first = toRef(array as any, key as any)
+      const fn = vi.fn()
+
+      effect(() => fn(first.value))
+      expect(fn).toHaveBeenCalledTimes(1)
+
+      triggerRef(first)
+      expect(fn).toHaveBeenCalledTimes(2)
+    }
+
+    assertTriggerRef(0)
+    // JS coerces non-symbol property keys like [0] to the string "0".
+    assertTriggerRef([0])
+  })
+
+  test('triggerRef on toRef created from symbol key preserves the symbol', () => {
+    const key = Symbol()
+    const object = reactive({ [key]: 'a' })
+    const value = toRef(object, key)
+    const fn = vi.fn()
+
+    effect(() => fn(value.value))
+    expect(fn).toHaveBeenCalledTimes(1)
+
+    triggerRef(value)
+    expect(fn).toHaveBeenCalledTimes(2)
+  })
+
   test('toRef default value', () => {
     const a: { x: number | undefined } = { x: undefined }
     const x = toRef(a, 'x', 1)
index 598b319f30ad59121beb457e09a5b469bdb9cb8a..594846d4fff26fef60b49500d2eaf6a728a2e7e3 100644 (file)
@@ -5,6 +5,7 @@ import {
   isFunction,
   isIntegerKey,
   isObject,
+  isSymbol,
 } from '@vue/shared'
 import { Dep, getDepFromReactive } from './dep'
 import {
@@ -333,6 +334,22 @@ export type ToRefs<T = any> = {
   [K in keyof T]: ToRef<T[K]>
 }
 
+type ArrayStringKey<T> = T extends readonly any[]
+  ? number extends T['length']
+    ? `${number}`
+    : never
+  : never
+
+type ToRefKey<T> = keyof T | ArrayStringKey<T>
+
+type ToRefValue<T extends object, K extends ToRefKey<T>> = K extends keyof T
+  ? T[K]
+  : T extends readonly (infer V)[]
+    ? K extends ArrayStringKey<T>
+      ? V
+      : never
+    : never
+
 /**
  * Converts a reactive object to a plain object where each property of the
  * resulting object is a ref pointing to the corresponding property of the
@@ -358,20 +375,22 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
   public _value: T[K] = undefined!
 
   private readonly _raw: T
+  private readonly _key: K
   private readonly _shallow: boolean
 
   constructor(
     private readonly _object: T,
-    private readonly _key: K,
+    key: K,
     private readonly _defaultValue?: T[K],
   ) {
+    this._key = (isSymbol(key) ? key : String(key)) as K
     this._raw = toRaw(_object)
 
     let shallow = true
     let obj = _object
 
     // For an array with integer key, refs are not unwrapped
-    if (!isArray(_object) || !isIntegerKey(String(_key))) {
+    if (!isArray(_object) || isSymbol(this._key) || !isIntegerKey(this._key)) {
       // Otherwise, check each proxy layer for unwrapping
       do {
         shallow = !isProxy(obj) || isShallow(obj)
@@ -469,19 +488,19 @@ export function toRef<T>(
   : T extends Ref
     ? T
     : Ref<UnwrapRef<T>>
-export function toRef<T extends object, K extends keyof T>(
+export function toRef<T extends object, K extends ToRefKey<T>>(
   object: T,
   key: K,
-): ToRef<T[K]>
-export function toRef<T extends object, K extends keyof T>(
+): ToRef<ToRefValue<T, K>>
+export function toRef<T extends object, K extends ToRefKey<T>>(
   object: T,
   key: K,
-  defaultValue: T[K],
-): ToRef<Exclude<T[K], undefined>>
+  defaultValue: ToRefValue<T, K>,
+): ToRef<Exclude<ToRefValue<T, K>, undefined>>
 /*@__NO_SIDE_EFFECTS__*/
 export function toRef(
-  source: Record<string, any> | MaybeRef,
-  key?: string,
+  source: Record<PropertyKey, any> | MaybeRef,
+  key?: string | number | symbol,
   defaultValue?: unknown,
 ): Ref {
   if (isRef(source)) {
@@ -496,8 +515,8 @@ export function toRef(
 }
 
 function propertyToRef(
-  source: Record<string, any>,
-  key: string,
+  source: Record<PropertyKey, any>,
+  key: string | number | symbol,
   defaultValue?: unknown,
 ) {
   return new ObjectRefImpl(source, key, defaultValue) as any