From c6a9787941ca99877d268182a5bb57fcf8b80b75 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Feb 2020 12:08:21 -0500 Subject: [PATCH] fix(types): ensure correct oldValue typing based on lazy option close #719 --- .../runtime-core/__tests__/apiWatch.spec.ts | 13 +++-- packages/runtime-core/src/apiWatch.ts | 36 ++++++++----- test-dts/watch.test-d.ts | 54 +++++++++++++++++++ 3 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 test-dts/watch.test-d.ts diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 82a4ad36..9b1d35a2 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -63,7 +63,9 @@ describe('api: watch', () => { dummy = [count, prevCount] // assert types count + 1 - prevCount + 1 + if (prevCount) { + prevCount + 1 + } } ) await nextTick() @@ -81,7 +83,9 @@ describe('api: watch', () => { dummy = [count, prevCount] // assert types count + 1 - prevCount + 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 - prevCount + 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 diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index c4bcdd58..1c930a09 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -37,9 +37,9 @@ export type WatchEffect = (onCleanup: CleanupRegistrator) => void export type WatchSource = Ref | ComputedRef | (() => T) -export type WatchCallback = ( - value: T, - oldValue: T, +export type WatchCallback = ( + value: V, + oldValue: OV, onCleanup: CleanupRegistrator ) => any @@ -47,10 +47,16 @@ type MapSources = { [K in keyof T]: T[K] extends WatchSource ? V : never } +type MapOldSources = { + [K in keyof T]: T[K] extends WatchSource + ? Lazy extends true ? V : (V | undefined) + : never +} + export type CleanupRegistrator = (invalidate: () => void) => void -export interface WatchOptions { - lazy?: boolean +export interface WatchOptions { + 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 +): StopHandle // overload #2: single source + cb -export function watch( +export function watch = false>( source: WatchSource, - cb: WatchCallback, - options?: WatchOptions + cb: WatchCallback, + options?: WatchOptions ): 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[]>>( +export function watch< + T extends Readonly[]>, + Lazy extends Readonly = false +>( sources: T, - cb: WatchCallback>, - options?: WatchOptions + cb: WatchCallback, MapOldSources>, + options?: WatchOptions ): StopHandle // implementation diff --git a/test-dts/watch.test-d.ts b/test-dts/watch.test-d.ts new file mode 100644 index 00000000..d32313cc --- /dev/null +++ b/test-dts/watch.test-d.ts @@ -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(value) + expectType(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>(values) + expectType< + Readonly<[string | undefined, string | undefined, number | undefined]> + >(oldValues) +}) + +// lazy watcher will have consistent types for oldValue. +watch( + source, + (value, oldValue) => { + expectType(value) + expectType(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>(values) + expectType>(oldValues) + }, + { lazy: true } +)