Skip to content

Commit c6a9787

Browse files
committed
fix(types): ensure correct oldValue typing based on lazy option
close #719
1 parent 8e19424 commit c6a9787

File tree

3 files changed

+88
-15
lines changed

3 files changed

+88
-15
lines changed

packages/runtime-core/__tests__/apiWatch.spec.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ describe('api: watch', () => {
6363
dummy = [count, prevCount]
6464
// assert types
6565
count + 1
66-
prevCount + 1
66+
if (prevCount) {
67+
prevCount + 1
68+
}
6769
}
6870
)
6971
await nextTick()
@@ -81,7 +83,9 @@ describe('api: watch', () => {
8183
dummy = [count, prevCount]
8284
// assert types
8385
count + 1
84-
prevCount + 1
86+
if (prevCount) {
87+
prevCount + 1
88+
}
8589
})
8690
await nextTick()
8791
expect(dummy).toMatchObject([0, undefined])
@@ -99,7 +103,9 @@ describe('api: watch', () => {
99103
dummy = [count, prevCount]
100104
// assert types
101105
count + 1
102-
prevCount + 1
106+
if (prevCount) {
107+
prevCount + 1
108+
}
103109
})
104110
await nextTick()
105111
expect(dummy).toMatchObject([1, undefined])
@@ -377,6 +383,7 @@ describe('api: watch', () => {
377383
it('ignore lazy option when using simple callback', async () => {
378384
const count = ref(0)
379385
let dummy
386+
// @ts-ignore
380387
watch(
381388
() => {
382389
dummy = count.value

packages/runtime-core/src/apiWatch.ts

+24-12
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,26 @@ export type WatchEffect = (onCleanup: CleanupRegistrator) => void
3737

3838
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
3939

40-
export type WatchCallback<T = any> = (
41-
value: T,
42-
oldValue: T,
40+
export type WatchCallback<V = any, OV = any> = (
41+
value: V,
42+
oldValue: OV,
4343
onCleanup: CleanupRegistrator
4444
) => any
4545

4646
type MapSources<T> = {
4747
[K in keyof T]: T[K] extends WatchSource<infer V> ? V : never
4848
}
4949

50+
type MapOldSources<T, Lazy> = {
51+
[K in keyof T]: T[K] extends WatchSource<infer V>
52+
? Lazy extends true ? V : (V | undefined)
53+
: never
54+
}
55+
5056
export type CleanupRegistrator = (invalidate: () => void) => void
5157

52-
export interface WatchOptions {
53-
lazy?: boolean
58+
export interface WatchOptions<Lazy = boolean> {
59+
lazy?: Lazy
5460
flush?: 'pre' | 'post' | 'sync'
5561
deep?: boolean
5662
onTrack?: ReactiveEffectOptions['onTrack']
@@ -65,23 +71,29 @@ const invoke = (fn: Function) => fn()
6571
const INITIAL_WATCHER_VALUE = {}
6672

6773
// overload #1: simple effect
68-
export function watch(effect: WatchEffect, options?: WatchOptions): StopHandle
74+
export function watch(
75+
effect: WatchEffect,
76+
options?: WatchOptions<false>
77+
): StopHandle
6978

7079
// overload #2: single source + cb
71-
export function watch<T>(
80+
export function watch<T, Lazy extends Readonly<boolean> = false>(
7281
source: WatchSource<T>,
73-
cb: WatchCallback<T>,
74-
options?: WatchOptions
82+
cb: WatchCallback<T, Lazy extends true ? T : (T | undefined)>,
83+
options?: WatchOptions<Lazy>
7584
): StopHandle
7685

7786
// overload #3: array of multiple sources + cb
7887
// Readonly constraint helps the callback to correctly infer value types based
7988
// on position in the source array. Otherwise the values will get a union type
8089
// of all possible value types.
81-
export function watch<T extends Readonly<WatchSource<unknown>[]>>(
90+
export function watch<
91+
T extends Readonly<WatchSource<unknown>[]>,
92+
Lazy extends Readonly<boolean> = false
93+
>(
8294
sources: T,
83-
cb: WatchCallback<MapSources<T>>,
84-
options?: WatchOptions
95+
cb: WatchCallback<MapSources<T>, MapOldSources<T, Lazy>>,
96+
options?: WatchOptions<Lazy>
8597
): StopHandle
8698

8799
// implementation

test-dts/watch.test-d.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ref, computed, watch } from './index'
2+
import { expectType } from 'tsd'
3+
4+
const source = ref('foo')
5+
const source2 = computed(() => source.value)
6+
const source3 = () => 1
7+
8+
// eager watcher's oldValue will be undefined on first run.
9+
watch(source, (value, oldValue) => {
10+
expectType<string>(value)
11+
expectType<string | undefined>(oldValue)
12+
})
13+
14+
watch([source, source2, source3], (values, oldValues) => {
15+
expectType<(string | number)[]>(values)
16+
expectType<(string | number | undefined)[]>(oldValues)
17+
})
18+
19+
// const array
20+
watch([source, source2, source3] as const, (values, oldValues) => {
21+
expectType<Readonly<[string, string, number]>>(values)
22+
expectType<
23+
Readonly<[string | undefined, string | undefined, number | undefined]>
24+
>(oldValues)
25+
})
26+
27+
// lazy watcher will have consistent types for oldValue.
28+
watch(
29+
source,
30+
(value, oldValue) => {
31+
expectType<string>(value)
32+
expectType<string>(oldValue)
33+
},
34+
{ lazy: true }
35+
)
36+
37+
watch(
38+
[source, source2, source3],
39+
(values, oldValues) => {
40+
expectType<(string | number)[]>(values)
41+
expectType<(string | number)[]>(oldValues)
42+
},
43+
{ lazy: true }
44+
)
45+
46+
// const array
47+
watch(
48+
[source, source2, source3] as const,
49+
(values, oldValues) => {
50+
expectType<Readonly<[string, string, number]>>(values)
51+
expectType<Readonly<[string, string, number]>>(oldValues)
52+
},
53+
{ lazy: true }
54+
)

0 commit comments

Comments
 (0)