Skip to content

Commit

Permalink
fix(types/apiWatch): correct type inference for reactive array (#11036)
Browse files Browse the repository at this point in the history
close #9416
  • Loading branch information
jh-leong committed Jun 10, 2024
1 parent ec424f6 commit aae2d78
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 18 deletions.
10 changes: 10 additions & 0 deletions packages/dts-test/reactivity.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])
})
58 changes: 58 additions & 0 deletions packages/dts-test/watch.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {
type ComputedRef,
type Ref,
computed,
defineComponent,
defineModel,
reactive,
ref,
shallowRef,
watch,
Expand All @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions packages/reactivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export {
type DeepReadonly,
type ShallowReactive,
type UnwrapNestedRefs,
type Reactive,
type ReactiveMarker,
} from './reactive'
export {
computed,
Expand Down
11 changes: 10 additions & 1 deletion packages/reactivity/src/reactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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)) {
Expand Down
33 changes: 16 additions & 17 deletions packages/runtime-core/src/apiWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type EffectScheduler,
ReactiveEffect,
ReactiveFlags,
type ReactiveMarker,
type Ref,
getCurrentScope,
isReactive,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export type {
DebuggerEvent,
DebuggerEventExtraInfo,
Raw,
Reactive,
} from '@vue/reactivity'
export type {
WatchEffect,
Expand Down

0 comments on commit aae2d78

Please sign in to comment.