Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

Commit aae2d78

Browse files
authored
fix(types/apiWatch): correct type inference for reactive array (#11036)
close #9416
1 parent ec424f6 commit aae2d78

File tree

6 files changed

+97
-18
lines changed

6 files changed

+97
-18
lines changed

packages/dts-test/reactivity.test-d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,13 @@ describe('should unwrap extended Set correctly', () => {
120120
expectType<string>(eset1.foo)
121121
expectType<number>(eset1.bar)
122122
})
123+
124+
describe('should not error when assignment', () => {
125+
const arr = reactive([''])
126+
let record: Record<number, string>
127+
record = arr
128+
expectType<string>(record[0])
129+
let record2: { [key: number]: string }
130+
record2 = arr
131+
expectType<string>(record2[0])
132+
})

packages/dts-test/watch.test-d.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import {
2+
type ComputedRef,
3+
type Ref,
24
computed,
35
defineComponent,
46
defineModel,
7+
reactive,
58
ref,
69
shallowRef,
710
watch,
@@ -12,8 +15,12 @@ const source = ref('foo')
1215
const source2 = computed(() => source.value)
1316
const source3 = () => 1
1417

18+
type Bar = Ref<string> | ComputedRef<string> | (() => number)
19+
type Foo = readonly [Ref<string>, ComputedRef<string>, () => number]
1520
type OnCleanup = (fn: () => void) => void
1621

22+
const readonlyArr: Foo = [source, source2, source3]
23+
1724
// lazy watcher will have consistent types for oldValue.
1825
watch(source, (value, oldValue, onCleanup) => {
1926
expectType<string>(value)
@@ -32,6 +39,29 @@ watch([source, source2, source3] as const, (values, oldValues) => {
3239
expectType<Readonly<[string, string, number]>>(oldValues)
3340
})
3441

42+
// reactive array
43+
watch(reactive([source, source2, source3]), (value, oldValues) => {
44+
expectType<Bar[]>(value)
45+
expectType<Bar[]>(oldValues)
46+
})
47+
48+
// reactive w/ readonly tuple
49+
watch(reactive([source, source2, source3] as const), (value, oldValues) => {
50+
expectType<Foo>(value)
51+
expectType<Foo>(oldValues)
52+
})
53+
54+
// readonly array
55+
watch(readonlyArr, (values, oldValues) => {
56+
expectType<Readonly<[string, string, number]>>(values)
57+
expectType<Readonly<[string, string, number]>>(oldValues)
58+
})
59+
60+
// no type error, case from vueuse
61+
declare const aAny: any
62+
watch(aAny, (v, ov) => {})
63+
watch(aAny, (v, ov) => {}, { immediate: true })
64+
3565
// immediate watcher's oldValue will be undefined on first run.
3666
watch(
3767
source,
@@ -65,6 +95,34 @@ watch(
6595
{ immediate: true },
6696
)
6797

98+
// reactive array
99+
watch(
100+
reactive([source, source2, source3]),
101+
(value, oldVals) => {
102+
expectType<Bar[]>(value)
103+
expectType<Bar[] | undefined>(oldVals)
104+
},
105+
{ immediate: true },
106+
)
107+
108+
// reactive w/ readonly tuple
109+
watch(reactive([source, source2, source3] as const), (value, oldVals) => {
110+
expectType<Foo>(value)
111+
expectType<Foo | undefined>(oldVals)
112+
})
113+
114+
// readonly array
115+
watch(
116+
readonlyArr,
117+
(values, oldValues) => {
118+
expectType<Readonly<[string, string, number]>>(values)
119+
expectType<
120+
Readonly<[string | undefined, string | undefined, number | undefined]>
121+
>(oldValues)
122+
},
123+
{ immediate: true },
124+
)
125+
68126
// should provide correct ref.value inner type to callbacks
69127
const nestedRefSource = ref({
70128
foo: ref(1),

packages/reactivity/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export {
3535
type DeepReadonly,
3636
type ShallowReactive,
3737
type UnwrapNestedRefs,
38+
type Reactive,
39+
type ReactiveMarker,
3840
} from './reactive'
3941
export {
4042
computed,

packages/reactivity/src/reactive.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ function getTargetType(value: Target) {
5858
// only unwrap nested ref
5959
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
6060

61+
declare const ReactiveMarkerSymbol: unique symbol
62+
63+
export declare class ReactiveMarker {
64+
private [ReactiveMarkerSymbol]?: void
65+
}
66+
67+
export type Reactive<T> = UnwrapNestedRefs<T> &
68+
(T extends readonly any[] ? ReactiveMarker : {})
69+
6170
/**
6271
* Returns a reactive proxy of the object.
6372
*
@@ -73,7 +82,7 @@ export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
7382
* @param target - The source object.
7483
* @see {@link https://vuejs.org/api/reactivity-core.html#reactive}
7584
*/
76-
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
85+
export function reactive<T extends object>(target: T): Reactive<T>
7786
export function reactive(target: object) {
7887
// if trying to observe a readonly proxy, return the readonly version.
7988
if (isReadonly(target)) {

packages/runtime-core/src/apiWatch.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type EffectScheduler,
55
ReactiveEffect,
66
ReactiveFlags,
7+
type ReactiveMarker,
78
type Ref,
89
getCurrentScope,
910
isReactive,
@@ -53,15 +54,13 @@ export type WatchCallback<V = any, OV = any> = (
5354
onCleanup: OnCleanup,
5455
) => any
5556

57+
type MaybeUndefined<T, I> = I extends true ? T | undefined : T
58+
5659
type MapSources<T, Immediate> = {
5760
[K in keyof T]: T[K] extends WatchSource<infer V>
58-
? Immediate extends true
59-
? V | undefined
60-
: V
61+
? MaybeUndefined<V, Immediate>
6162
: T[K] extends object
62-
? Immediate extends true
63-
? T[K] | undefined
64-
: T[K]
63+
? MaybeUndefined<T[K], Immediate>
6564
: never
6665
}
6766

@@ -117,28 +116,28 @@ type MultiWatchSources = (WatchSource<unknown> | object)[]
117116
// overload: single source + cb
118117
export function watch<T, Immediate extends Readonly<boolean> = false>(
119118
source: WatchSource<T>,
120-
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
119+
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
121120
options?: WatchOptions<Immediate>,
122121
): WatchStopHandle
123122

124-
// overload: array of multiple sources + cb
123+
// overload: reactive array or tuple of multiple sources + cb
125124
export function watch<
126-
T extends MultiWatchSources,
125+
T extends Readonly<MultiWatchSources>,
127126
Immediate extends Readonly<boolean> = false,
128127
>(
129-
sources: [...T],
130-
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
128+
sources: readonly [...T] | T,
129+
cb: [T] extends [ReactiveMarker]
130+
? WatchCallback<T, MaybeUndefined<T, Immediate>>
131+
: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
131132
options?: WatchOptions<Immediate>,
132133
): WatchStopHandle
133134

134-
// overload: multiple sources w/ `as const`
135-
// watch([foo, bar] as const, () => {})
136-
// somehow [...T] breaks when the type is readonly
135+
// overload: array of multiple sources + cb
137136
export function watch<
138-
T extends Readonly<MultiWatchSources>,
137+
T extends MultiWatchSources,
139138
Immediate extends Readonly<boolean> = false,
140139
>(
141-
source: T,
140+
sources: [...T],
142141
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
143142
options?: WatchOptions<Immediate>,
144143
): WatchStopHandle
@@ -149,7 +148,7 @@ export function watch<
149148
Immediate extends Readonly<boolean> = false,
150149
>(
151150
source: T,
152-
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
151+
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
153152
options?: WatchOptions<Immediate>,
154153
): WatchStopHandle
155154

packages/runtime-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ export type {
212212
DebuggerEvent,
213213
DebuggerEventExtraInfo,
214214
Raw,
215+
Reactive,
215216
} from '@vue/reactivity'
216217
export type {
217218
WatchEffect,

0 commit comments

Comments
 (0)