]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(types/apiWatch): correct type inference for reactive array (#11036)
authorTycho <jh.leong@outlook.com>
Mon, 10 Jun 2024 08:07:25 +0000 (16:07 +0800)
committerGitHub <noreply@github.com>
Mon, 10 Jun 2024 08:07:25 +0000 (16:07 +0800)
close #9416

packages/dts-test/reactivity.test-d.ts
packages/dts-test/watch.test-d.ts
packages/reactivity/src/index.ts
packages/reactivity/src/reactive.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/index.ts

index ec220f68198e71c8bc6eb85dc40f82ab6a512f97..f5b9176729c341df737fd019d343da045cf30358 100644 (file)
@@ -120,3 +120,13 @@ describe('should unwrap extended Set correctly', () => {
   expectType<string>(eset1.foo)
   expectType<number>(eset1.bar)
 })
+
+describe('should not error when assignment', () => {
+  const arr = reactive([''])
+  let record: Record<number, string>
+  record = arr
+  expectType<string>(record[0])
+  let record2: { [key: number]: string }
+  record2 = arr
+  expectType<string>(record2[0])
+})
index 507fb6f5dcd9317bfa276699f8511544731f9b8f..45c898ef672ad50c5f07bdd64e4b19404a834910 100644 (file)
@@ -1,7 +1,10 @@
 import {
+  type ComputedRef,
+  type Ref,
   computed,
   defineComponent,
   defineModel,
+  reactive,
   ref,
   shallowRef,
   watch,
@@ -12,8 +15,12 @@ const source = ref('foo')
 const source2 = computed(() => source.value)
 const source3 = () => 1
 
+type Bar = Ref<string> | ComputedRef<string> | (() => number)
+type Foo = readonly [Ref<string>, ComputedRef<string>, () => number]
 type OnCleanup = (fn: () => void) => void
 
+const readonlyArr: Foo = [source, source2, source3]
+
 // lazy watcher will have consistent types for oldValue.
 watch(source, (value, oldValue, onCleanup) => {
   expectType<string>(value)
@@ -32,6 +39,29 @@ watch([source, source2, source3] as const, (values, oldValues) => {
   expectType<Readonly<[string, string, number]>>(oldValues)
 })
 
+// reactive array
+watch(reactive([source, source2, source3]), (value, oldValues) => {
+  expectType<Bar[]>(value)
+  expectType<Bar[]>(oldValues)
+})
+
+// reactive w/ readonly tuple
+watch(reactive([source, source2, source3] as const), (value, oldValues) => {
+  expectType<Foo>(value)
+  expectType<Foo>(oldValues)
+})
+
+// readonly array
+watch(readonlyArr, (values, oldValues) => {
+  expectType<Readonly<[string, string, number]>>(values)
+  expectType<Readonly<[string, string, number]>>(oldValues)
+})
+
+// no type error, case from vueuse
+declare const aAny: any
+watch(aAny, (v, ov) => {})
+watch(aAny, (v, ov) => {}, { immediate: true })
+
 // immediate watcher's oldValue will be undefined on first run.
 watch(
   source,
@@ -65,6 +95,34 @@ watch(
   { immediate: true },
 )
 
+// reactive array
+watch(
+  reactive([source, source2, source3]),
+  (value, oldVals) => {
+    expectType<Bar[]>(value)
+    expectType<Bar[] | undefined>(oldVals)
+  },
+  { immediate: true },
+)
+
+// reactive w/ readonly tuple
+watch(reactive([source, source2, source3] as const), (value, oldVals) => {
+  expectType<Foo>(value)
+  expectType<Foo | undefined>(oldVals)
+})
+
+// readonly array
+watch(
+  readonlyArr,
+  (values, oldValues) => {
+    expectType<Readonly<[string, string, number]>>(values)
+    expectType<
+      Readonly<[string | undefined, string | undefined, number | undefined]>
+    >(oldValues)
+  },
+  { immediate: true },
+)
+
 // should provide correct ref.value inner type to callbacks
 const nestedRefSource = ref({
   foo: ref(1),
index 505ec9e2035939e4d6704c7b647d1086546d7f53..f36b7f425d53d33189297de404e6dd302fae4a22 100644 (file)
@@ -35,6 +35,8 @@ export {
   type DeepReadonly,
   type ShallowReactive,
   type UnwrapNestedRefs,
+  type Reactive,
+  type ReactiveMarker,
 } from './reactive'
 export {
   computed,
index 6e28be404fa24551fe3c09e9b9b1f79a07caf843..656cceb370d81afb8e77785bee88e79660082a79 100644 (file)
@@ -58,6 +58,15 @@ function getTargetType(value: Target) {
 // only unwrap nested ref
 export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
 
+declare const ReactiveMarkerSymbol: unique symbol
+
+export declare class ReactiveMarker {
+  private [ReactiveMarkerSymbol]?: void
+}
+
+export type Reactive<T> = UnwrapNestedRefs<T> &
+  (T extends readonly any[] ? ReactiveMarker : {})
+
 /**
  * Returns a reactive proxy of the object.
  *
@@ -73,7 +82,7 @@ export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
  * @param target - The source object.
  * @see {@link https://vuejs.org/api/reactivity-core.html#reactive}
  */
-export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
+export function reactive<T extends object>(target: T): Reactive<T>
 export function reactive(target: object) {
   // if trying to observe a readonly proxy, return the readonly version.
   if (isReadonly(target)) {
index bab9e0764f5b05f939d1f372c18b33adde11c8bf..cdf8b8c888f1546f42d9cb95765bb77c4c57eb17 100644 (file)
@@ -4,6 +4,7 @@ import {
   type EffectScheduler,
   ReactiveEffect,
   ReactiveFlags,
+  type ReactiveMarker,
   type Ref,
   getCurrentScope,
   isReactive,
@@ -53,15 +54,13 @@ export type WatchCallback<V = any, OV = any> = (
   onCleanup: OnCleanup,
 ) => any
 
+type MaybeUndefined<T, I> = I extends true ? T | undefined : T
+
 type MapSources<T, Immediate> = {
   [K in keyof T]: T[K] extends WatchSource<infer V>
-    ? Immediate extends true
-      ? V | undefined
-      : V
+    ? MaybeUndefined<V, Immediate>
     : T[K] extends object
-      ? Immediate extends true
-        ? T[K] | undefined
-        : T[K]
+      ? MaybeUndefined<T[K], Immediate>
       : never
 }
 
@@ -117,28 +116,28 @@ type MultiWatchSources = (WatchSource<unknown> | object)[]
 // overload: single source + cb
 export function watch<T, Immediate extends Readonly<boolean> = false>(
   source: WatchSource<T>,
-  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
+  cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
   options?: WatchOptions<Immediate>,
 ): WatchStopHandle
 
-// overload: array of multiple sources + cb
+// overload: reactive array or tuple of multiple sources + cb
 export function watch<
-  T extends MultiWatchSources,
+  T extends Readonly<MultiWatchSources>,
   Immediate extends Readonly<boolean> = false,
 >(
-  sources: [...T],
-  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
+  sources: readonly [...T] | T,
+  cb: [T] extends [ReactiveMarker]
+    ? WatchCallback<T, MaybeUndefined<T, Immediate>>
+    : WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
   options?: WatchOptions<Immediate>,
 ): WatchStopHandle
 
-// overload: multiple sources w/ `as const`
-// watch([foo, bar] as const, () => {})
-// somehow [...T] breaks when the type is readonly
+// overload: array of multiple sources + cb
 export function watch<
-  T extends Readonly<MultiWatchSources>,
+  T extends MultiWatchSources,
   Immediate extends Readonly<boolean> = false,
 >(
-  source: T,
+  sources: [...T],
   cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
   options?: WatchOptions<Immediate>,
 ): WatchStopHandle
@@ -149,7 +148,7 @@ export function watch<
   Immediate extends Readonly<boolean> = false,
 >(
   source: T,
-  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
+  cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
   options?: WatchOptions<Immediate>,
 ): WatchStopHandle
 
index 94b2985040acfeadf07ce890e0de470b2d5f5d3d..d498728820669f7ee0280f46284a1e53dbc7e8fa 100644 (file)
@@ -212,6 +212,7 @@ export type {
   DebuggerEvent,
   DebuggerEventExtraInfo,
   Raw,
+  Reactive,
 } from '@vue/reactivity'
 export type {
   WatchEffect,