From aae2d78875daa476280a45e71c2f38292964efae Mon Sep 17 00:00:00 2001 From: Tycho Date: Mon, 10 Jun 2024 16:07:25 +0800 Subject: [PATCH] fix(types/apiWatch): correct type inference for reactive array (#11036) close #9416 --- packages/dts-test/reactivity.test-d.ts | 10 +++++ packages/dts-test/watch.test-d.ts | 58 ++++++++++++++++++++++++++ packages/reactivity/src/index.ts | 2 + packages/reactivity/src/reactive.ts | 11 ++++- packages/runtime-core/src/apiWatch.ts | 33 +++++++-------- packages/runtime-core/src/index.ts | 1 + 6 files changed, 97 insertions(+), 18 deletions(-) diff --git a/packages/dts-test/reactivity.test-d.ts b/packages/dts-test/reactivity.test-d.ts index ec220f68198..f5b9176729c 100644 --- a/packages/dts-test/reactivity.test-d.ts +++ b/packages/dts-test/reactivity.test-d.ts @@ -120,3 +120,13 @@ describe('should unwrap extended Set correctly', () => { expectType(eset1.foo) expectType(eset1.bar) }) + +describe('should not error when assignment', () => { + const arr = reactive(['']) + let record: Record + record = arr + expectType(record[0]) + let record2: { [key: number]: string } + record2 = arr + expectType(record2[0]) +}) diff --git a/packages/dts-test/watch.test-d.ts b/packages/dts-test/watch.test-d.ts index 507fb6f5dcd..45c898ef672 100644 --- a/packages/dts-test/watch.test-d.ts +++ b/packages/dts-test/watch.test-d.ts @@ -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 | ComputedRef | (() => number) +type Foo = readonly [Ref, ComputedRef, () => 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(value) @@ -32,6 +39,29 @@ watch([source, source2, source3] as const, (values, oldValues) => { expectType>(oldValues) }) +// reactive array +watch(reactive([source, source2, source3]), (value, oldValues) => { + expectType(value) + expectType(oldValues) +}) + +// reactive w/ readonly tuple +watch(reactive([source, source2, source3] as const), (value, oldValues) => { + expectType(value) + expectType(oldValues) +}) + +// readonly array +watch(readonlyArr, (values, oldValues) => { + expectType>(values) + expectType>(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(value) + expectType(oldVals) + }, + { immediate: true }, +) + +// reactive w/ readonly tuple +watch(reactive([source, source2, source3] as const), (value, oldVals) => { + expectType(value) + expectType(oldVals) +}) + +// readonly array +watch( + readonlyArr, + (values, oldValues) => { + expectType>(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), diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 505ec9e2035..f36b7f425d5 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -35,6 +35,8 @@ export { type DeepReadonly, type ShallowReactive, type UnwrapNestedRefs, + type Reactive, + type ReactiveMarker, } from './reactive' export { computed, diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index 6e28be404fa..656cceb370d 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -58,6 +58,15 @@ function getTargetType(value: Target) { // only unwrap nested ref export type UnwrapNestedRefs = T extends Ref ? T : UnwrapRefSimple +declare const ReactiveMarkerSymbol: unique symbol + +export declare class ReactiveMarker { + private [ReactiveMarkerSymbol]?: void +} + +export type Reactive = UnwrapNestedRefs & + (T extends readonly any[] ? ReactiveMarker : {}) + /** * Returns a reactive proxy of the object. * @@ -73,7 +82,7 @@ export type UnwrapNestedRefs = T extends Ref ? T : UnwrapRefSimple * @param target - The source object. * @see {@link https://vuejs.org/api/reactivity-core.html#reactive} */ -export function reactive(target: T): UnwrapNestedRefs +export function reactive(target: T): Reactive export function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (isReadonly(target)) { diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index bab9e0764f5..cdf8b8c888f 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -4,6 +4,7 @@ import { type EffectScheduler, ReactiveEffect, ReactiveFlags, + type ReactiveMarker, type Ref, getCurrentScope, isReactive, @@ -53,15 +54,13 @@ export type WatchCallback = ( onCleanup: OnCleanup, ) => any +type MaybeUndefined = I extends true ? T | undefined : T + type MapSources = { [K in keyof T]: T[K] extends WatchSource - ? Immediate extends true - ? V | undefined - : V + ? MaybeUndefined : T[K] extends object - ? Immediate extends true - ? T[K] | undefined - : T[K] + ? MaybeUndefined : never } @@ -117,28 +116,28 @@ type MultiWatchSources = (WatchSource | object)[] // overload: single source + cb export function watch = false>( source: WatchSource, - cb: WatchCallback, + cb: WatchCallback>, options?: WatchOptions, ): 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, Immediate extends Readonly = false, >( - sources: [...T], - cb: WatchCallback, MapSources>, + sources: readonly [...T] | T, + cb: [T] extends [ReactiveMarker] + ? WatchCallback> + : WatchCallback, MapSources>, options?: WatchOptions, ): 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, + T extends MultiWatchSources, Immediate extends Readonly = false, >( - source: T, + sources: [...T], cb: WatchCallback, MapSources>, options?: WatchOptions, ): WatchStopHandle @@ -149,7 +148,7 @@ export function watch< Immediate extends Readonly = false, >( source: T, - cb: WatchCallback, + cb: WatchCallback>, options?: WatchOptions, ): WatchStopHandle diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 94b2985040a..d4987288206 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -212,6 +212,7 @@ export type { DebuggerEvent, DebuggerEventExtraInfo, Raw, + Reactive, } from '@vue/reactivity' export type { WatchEffect,