Skip to content

Commit

Permalink
fix(type): align watch types with vue3 (#927)
Browse files Browse the repository at this point in the history
  • Loading branch information
MinatoHikari committed Apr 27, 2022
1 parent 293f03b commit 679f5c2
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 22 deletions.
48 changes: 33 additions & 15 deletions src/apis/watch.ts
Expand Up @@ -32,18 +32,16 @@ export type WatchCallback<V = any, OV = any> = (
onInvalidate: InvalidateCbRegistrator
) => any

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

type MapOldSources<T, Immediate> = {
declare type MapSources<T, Immediate> = {
[K in keyof T]: T[K] extends WatchSource<infer V>
? Immediate extends true
? V | undefined
: V
: never
}

type MultiWatchSources = (WatchSource<unknown> | object)[]

export interface WatchOptionsBase {
flush?: FlushMode
// onTrack?: ReactiveEffectOptions['onTrack'];
Expand Down Expand Up @@ -204,8 +202,8 @@ function patchWatcherTeardown(watcher: VueWatcher, runCleanup: () => void) {

function createWatcher(
vm: ComponentInstance,
source: WatchSource<unknown> | WatchSource<unknown>[] | WatchEffect,
cb: WatchCallback<any> | null,
source: WatchSource | WatchSource[] | WatchEffect,
cb: WatchCallback | null,
options: WatchOptions
): () => void {
if (__DEV__ && !cb) {
Expand Down Expand Up @@ -416,27 +414,47 @@ export function watchSyncEffect(effect: WatchEffect) {
return watchEffect(effect, { flush: 'sync' })
}

// overload #1: 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.
// overload #1 + #2 + #3: array of multiple sources + cb

// overload #1: In readonly case the first overload must be spread tuple type.
// In otherwise members in tuple can not get the correct types.
export function watch<
T extends Readonly<MultiWatchSources>,
Immediate extends Readonly<boolean> = false
>(
sources: [...T],
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle

// overload #2: for not spread readonly tuple
export function watch<
T extends Readonly<MultiWatchSources>,
Immediate extends Readonly<boolean> = false
>(
sources: T,
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle

// overload #3: for not readonly multiSources
export function watch<
T extends Readonly<WatchSource<unknown>[]>,
T extends MultiWatchSources,
Immediate extends Readonly<boolean> = false
>(
sources: [...T],
cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>,
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle

// overload #2: single source + cb
// overload #4: single source + cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle

// overload #3: watching reactive object w/ cb
// overload #5: watching reactive object w/ cb
export function watch<
T extends object,
Immediate extends Readonly<boolean> = false
Expand Down
39 changes: 33 additions & 6 deletions test-dts/watch.test-d.tsx
Expand Up @@ -10,17 +10,44 @@ watch(source, (value, oldValue) => {
expectType<string>(oldValue)
})

watch([source, source2, source3], (values, oldValues) => {
expectType<(string | number)[]>(values)
expectType<(string | number)[]>(oldValues)
})
// spread array
watch(
[source, source2, source3],
([source1, source2, source3], [oldSource1, oldSource2, oldSource3]) => {
expectType<string>(source1)
expectType<string>(source2)
expectType<number>(source3)
expectType<string>(oldSource1)
expectType<string>(oldSource2)
expectType<number>(oldSource3)
}
)

// const array
watch([source, source2, source3], (values, oldValues) => {
watch([source, source2, source3] as const, (values, oldValues) => {
expectType<Readonly<[string, string, number]>>(values)
expectType<Readonly<[string, string, number]>>(oldValues)
expectType<string>(values[0])
expectType<string>(values[1])
expectType<number>(values[2])
expectType<string>(oldValues[0])
expectType<string>(oldValues[1])
expectType<number>(oldValues[2])
})

// const spread array
watch(
[source, source2, source3] as const,
([source1, source2, source3], [oldSource1, oldSource2, oldSource3]) => {
expectType<string>(source1)
expectType<string>(source2)
expectType<number>(source3)
expectType<string>(oldSource1)
expectType<string>(oldSource2)
expectType<number>(oldSource3)
}
)

// immediate watcher's oldValue will be undefined on first run.
watch(
source,
Expand All @@ -42,7 +69,7 @@ watch(

// const array
watch(
[source, source2, source3],
[source, source2, source3] as const,
(values, oldValues) => {
expectType<Readonly<[string, string, number]>>(values)
expectType<
Expand Down
2 changes: 1 addition & 1 deletion test/v3/runtime-core/apiWatch.spec.ts
Expand Up @@ -143,7 +143,7 @@ describe('api: watch', () => {
const status = ref(false)

let dummy
watch([() => state.count, status], (vals, oldVals) => {
watch([() => state.count, status] as const, (vals, oldVals) => {
dummy = [vals, oldVals]
const [count] = vals
const [, oldStatus] = oldVals
Expand Down

0 comments on commit 679f5c2

Please sign in to comment.