Skip to content

Commit 1bb28d0

Browse files
authored
fix(reactivity): normalize toRef property keys before dep lookup + improve types (#14625)
close #12427 close #12431
1 parent 959ded2 commit 1bb28d0

3 files changed

Lines changed: 69 additions & 11 deletions

File tree

packages-private/dts-test/ref.test-d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,14 @@ expectType<{ name: string } | null>(p2.union)
338338
// Should not distribute Refs over union
339339
expectType<Ref<number | string>>(toRef(obj, 'c'))
340340

341+
const array = reactive(['a', 'b'])
342+
expectType<Ref<string>>(toRef(array, '1'))
343+
expectType<Ref<string>>(toRef(array, '1', 'fallback'))
344+
345+
const tuple: [string, number] = ['a', 1]
346+
expectType<Ref<string>>(toRef(tuple, '0'))
347+
expectType<Ref<number>>(toRef(tuple, '1'))
348+
341349
expectType<Ref<number>>(toRef(() => 123))
342350
expectType<Ref<number | string>>(toRef(() => obj.c))
343351

packages/reactivity/__tests__/ref.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,37 @@ describe('reactivity/ref', () => {
388388
expect(bar.value).toBe(6)
389389
})
390390

391+
test('triggerRef on toRef created from array coerces property keys', () => {
392+
const assertTriggerRef = (key: unknown) => {
393+
const array = reactive(['a'])
394+
const first = toRef(array as any, key as any)
395+
const fn = vi.fn()
396+
397+
effect(() => fn(first.value))
398+
expect(fn).toHaveBeenCalledTimes(1)
399+
400+
triggerRef(first)
401+
expect(fn).toHaveBeenCalledTimes(2)
402+
}
403+
404+
assertTriggerRef(0)
405+
// JS coerces non-symbol property keys like [0] to the string "0".
406+
assertTriggerRef([0])
407+
})
408+
409+
test('triggerRef on toRef created from symbol key preserves the symbol', () => {
410+
const key = Symbol()
411+
const object = reactive({ [key]: 'a' })
412+
const value = toRef(object, key)
413+
const fn = vi.fn()
414+
415+
effect(() => fn(value.value))
416+
expect(fn).toHaveBeenCalledTimes(1)
417+
418+
triggerRef(value)
419+
expect(fn).toHaveBeenCalledTimes(2)
420+
})
421+
391422
test('toRef default value', () => {
392423
const a: { x: number | undefined } = { x: undefined }
393424
const x = toRef(a, 'x', 1)

packages/reactivity/src/ref.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isFunction,
66
isIntegerKey,
77
isObject,
8+
isSymbol,
89
} from '@vue/shared'
910
import { Dep, getDepFromReactive } from './dep'
1011
import {
@@ -333,6 +334,22 @@ export type ToRefs<T = any> = {
333334
[K in keyof T]: ToRef<T[K]>
334335
}
335336

337+
type ArrayStringKey<T> = T extends readonly any[]
338+
? number extends T['length']
339+
? `${number}`
340+
: never
341+
: never
342+
343+
type ToRefKey<T> = keyof T | ArrayStringKey<T>
344+
345+
type ToRefValue<T extends object, K extends ToRefKey<T>> = K extends keyof T
346+
? T[K]
347+
: T extends readonly (infer V)[]
348+
? K extends ArrayStringKey<T>
349+
? V
350+
: never
351+
: never
352+
336353
/**
337354
* Converts a reactive object to a plain object where each property of the
338355
* 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> {
358375
public _value: T[K] = undefined!
359376

360377
private readonly _raw: T
378+
private readonly _key: K
361379
private readonly _shallow: boolean
362380

363381
constructor(
364382
private readonly _object: T,
365-
private readonly _key: K,
383+
key: K,
366384
private readonly _defaultValue?: T[K],
367385
) {
386+
this._key = (isSymbol(key) ? key : String(key)) as K
368387
this._raw = toRaw(_object)
369388

370389
let shallow = true
371390
let obj = _object
372391

373392
// For an array with integer key, refs are not unwrapped
374-
if (!isArray(_object) || !isIntegerKey(String(_key))) {
393+
if (!isArray(_object) || isSymbol(this._key) || !isIntegerKey(this._key)) {
375394
// Otherwise, check each proxy layer for unwrapping
376395
do {
377396
shallow = !isProxy(obj) || isShallow(obj)
@@ -469,19 +488,19 @@ export function toRef<T>(
469488
: T extends Ref
470489
? T
471490
: Ref<UnwrapRef<T>>
472-
export function toRef<T extends object, K extends keyof T>(
491+
export function toRef<T extends object, K extends ToRefKey<T>>(
473492
object: T,
474493
key: K,
475-
): ToRef<T[K]>
476-
export function toRef<T extends object, K extends keyof T>(
494+
): ToRef<ToRefValue<T, K>>
495+
export function toRef<T extends object, K extends ToRefKey<T>>(
477496
object: T,
478497
key: K,
479-
defaultValue: T[K],
480-
): ToRef<Exclude<T[K], undefined>>
498+
defaultValue: ToRefValue<T, K>,
499+
): ToRef<Exclude<ToRefValue<T, K>, undefined>>
481500
/*@__NO_SIDE_EFFECTS__*/
482501
export function toRef(
483-
source: Record<string, any> | MaybeRef,
484-
key?: string,
502+
source: Record<PropertyKey, any> | MaybeRef,
503+
key?: string | number | symbol,
485504
defaultValue?: unknown,
486505
): Ref {
487506
if (isRef(source)) {
@@ -496,8 +515,8 @@ export function toRef(
496515
}
497516

498517
function propertyToRef(
499-
source: Record<string, any>,
500-
key: string,
518+
source: Record<PropertyKey, any>,
519+
key: string | number | symbol,
501520
defaultValue?: unknown,
502521
) {
503522
return new ObjectRefImpl(source, key, defaultValue) as any

0 commit comments

Comments
 (0)