fix(types): ensure correct oldValue typing based on lazy option

close #719
This commit is contained in:
Evan You 2020-02-13 12:08:21 -05:00
parent 8e19424c04
commit c6a9787941
3 changed files with 88 additions and 15 deletions

View File

@ -63,8 +63,10 @@ describe('api: watch', () => {
dummy = [count, prevCount]
// assert types
count + 1
if (prevCount) {
prevCount + 1
}
}
)
await nextTick()
expect(dummy).toMatchObject([0, undefined])
@ -81,7 +83,9 @@ describe('api: watch', () => {
dummy = [count, prevCount]
// assert types
count + 1
if (prevCount) {
prevCount + 1
}
})
await nextTick()
expect(dummy).toMatchObject([0, undefined])
@ -99,7 +103,9 @@ describe('api: watch', () => {
dummy = [count, prevCount]
// assert types
count + 1
if (prevCount) {
prevCount + 1
}
})
await nextTick()
expect(dummy).toMatchObject([1, undefined])
@ -377,6 +383,7 @@ describe('api: watch', () => {
it('ignore lazy option when using simple callback', async () => {
const count = ref(0)
let dummy
// @ts-ignore
watch(
() => {
dummy = count.value

View File

@ -37,9 +37,9 @@ export type WatchEffect = (onCleanup: CleanupRegistrator) => void
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
export type WatchCallback<T = any> = (
value: T,
oldValue: T,
export type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: CleanupRegistrator
) => any
@ -47,10 +47,16 @@ type MapSources<T> = {
[K in keyof T]: T[K] extends WatchSource<infer V> ? V : never
}
type MapOldSources<T, Lazy> = {
[K in keyof T]: T[K] extends WatchSource<infer V>
? Lazy extends true ? V : (V | undefined)
: never
}
export type CleanupRegistrator = (invalidate: () => void) => void
export interface WatchOptions {
lazy?: boolean
export interface WatchOptions<Lazy = boolean> {
lazy?: Lazy
flush?: 'pre' | 'post' | 'sync'
deep?: boolean
onTrack?: ReactiveEffectOptions['onTrack']
@ -65,23 +71,29 @@ const invoke = (fn: Function) => fn()
const INITIAL_WATCHER_VALUE = {}
// overload #1: simple effect
export function watch(effect: WatchEffect, options?: WatchOptions): StopHandle
export function watch(
effect: WatchEffect,
options?: WatchOptions<false>
): StopHandle
// overload #2: single source + cb
export function watch<T>(
export function watch<T, Lazy extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T>,
options?: WatchOptions
cb: WatchCallback<T, Lazy extends true ? T : (T | undefined)>,
options?: WatchOptions<Lazy>
): StopHandle
// overload #3: array of multiple sources + cb
// Readonly constraint helps the callback to correctly infer value types based
// on position in the source array. Otherwise the values will get a union type
// of all possible value types.
export function watch<T extends Readonly<WatchSource<unknown>[]>>(
export function watch<
T extends Readonly<WatchSource<unknown>[]>,
Lazy extends Readonly<boolean> = false
>(
sources: T,
cb: WatchCallback<MapSources<T>>,
options?: WatchOptions
cb: WatchCallback<MapSources<T>, MapOldSources<T, Lazy>>,
options?: WatchOptions<Lazy>
): StopHandle
// implementation

54
test-dts/watch.test-d.ts Normal file
View File

@ -0,0 +1,54 @@
import { ref, computed, watch } from './index'
import { expectType } from 'tsd'
const source = ref('foo')
const source2 = computed(() => source.value)
const source3 = () => 1
// eager watcher's oldValue will be undefined on first run.
watch(source, (value, oldValue) => {
expectType<string>(value)
expectType<string | undefined>(oldValue)
})
watch([source, source2, source3], (values, oldValues) => {
expectType<(string | number)[]>(values)
expectType<(string | number | undefined)[]>(oldValues)
})
// const array
watch([source, source2, source3] as const, (values, oldValues) => {
expectType<Readonly<[string, string, number]>>(values)
expectType<
Readonly<[string | undefined, string | undefined, number | undefined]>
>(oldValues)
})
// lazy watcher will have consistent types for oldValue.
watch(
source,
(value, oldValue) => {
expectType<string>(value)
expectType<string>(oldValue)
},
{ lazy: true }
)
watch(
[source, source2, source3],
(values, oldValues) => {
expectType<(string | number)[]>(values)
expectType<(string | number)[]>(oldValues)
},
{ lazy: true }
)
// const array
watch(
[source, source2, source3] as const,
(values, oldValues) => {
expectType<Readonly<[string, string, number]>>(values)
expectType<Readonly<[string, string, number]>>(oldValues)
},
{ lazy: true }
)