Skip to content

Commit

Permalink
feat(syncRef): enhance syncRef type restrict
Browse files Browse the repository at this point in the history
fix#3514
  • Loading branch information
Doctor-wu committed Oct 31, 2023
1 parent bacf402 commit 8495e56
Show file tree
Hide file tree
Showing 2 changed files with 269 additions and 13 deletions.
145 changes: 145 additions & 0 deletions packages/shared/syncRef/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,149 @@ describe('syncRef', () => {
expect(right.value).toBe(10)
expect(left.value).toBe(5)
})

it('ts works', () => {
const ref0 = ref(0)
const ref1 = ref(1)
const refString = ref('1')
const refNumString = ref<number | string>(1)
const refNumBoolean = ref<number | boolean>(1)
// L = A && direction === 'both'
syncRef(ref0, ref1)()
syncRef(ref0, ref1, {
direction: 'both',
})()
syncRef(ref0, ref1, {
direction: 'both',
transform: {},
})()
syncRef(ref0, ref1, {
direction: 'both',
transform: {
ltr: v => v,
},
})()
syncRef(ref0, ref1, {
direction: 'both',
transform: {
rtl: v => v,
},
})()
syncRef(ref0, ref1, {
direction: 'both',
transform: {
ltr: v => v,
rtl: v => v,
},
})()
syncRef(ref0, ref1, {
direction: 'both',
transform: {
// @ts-expect-error wrong type, should be (left: L) => R
ltr: v => v.toString(),
rtl: v => v,
},
})()
// L = A && direction === 'ltr'
syncRef(ref0, ref1, {
direction: 'ltr',
})()
syncRef(ref0, ref1, {
direction: 'ltr',
transform: {},
})()
syncRef(ref0, ref1, {
direction: 'ltr',
transform: {
ltr: v => v,
},
})()
syncRef(ref0, ref1, {
direction: 'ltr',
transform: {
// @ts-expect-error wrong transform type, should be ltr
rtl: v => v,
},
})()
// L = A && direction === 'rtl'
syncRef(ref0, ref1, {
direction: 'rtl',
})()
syncRef(ref0, ref1, {
direction: 'rtl',
transform: {},
})()
syncRef(ref0, ref1, {
direction: 'rtl',
transform: {
rtl: v => v,
},
})()
// L ⊆ R && direction === 'both'
// @ts-expect-error wrong type, should provide transform
syncRef(ref0, refNumString, {
direction: 'both',
})()
syncRef(ref0, refNumString, {
direction: 'both',
transform: {
ltr: v => v.toString(),
rtl: v => Number(v),
},
})()
// L ⊆ R && direction === 'ltr'
syncRef(ref0, refNumString, {
direction: 'ltr',
transform: {
ltr: v => v.toString(),
},
})()
// L ⊆ R && direction === 'rtl'
syncRef(ref0, refNumString, {
direction: 'ltr',
transform: {
ltr: v => Number(v),
},
})()
// L ∩ R = ∅ && direction === 'both'
syncRef(ref0, refString, {
direction: 'both',
transform: {
ltr: v => v.toString(),
rtl: v => Number(v),
},
})()
// L ∩ R = ∅ && direction === 'ltr'
syncRef(ref0, refString, {
direction: 'ltr',
transform: {
ltr: v => v.toString(),
},
})()
// L ∩ R = ∅ && direction === 'rtl'
syncRef(ref0, refString, {
direction: 'rtl',
transform: {
rtl: v => Number(v),
},
})()
// L ∩ R = ∅ && direction === 'both'
syncRef(ref0, refString, {
direction: 'both',
// @ts-expect-error wrong type, should provide ltr
transform: {
rtl: v => Number(v),
},
})()
// L ∩ R ≠ ∅
syncRef(refNumString, refNumBoolean, {
transform: {
ltr: v => Number(v),
rtl: v => Number(v),
},
})

// @ts-expect-error lack of options
syncRef(ref0, refString)()
})
})
137 changes: 124 additions & 13 deletions packages/shared/syncRef/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,48 @@
import type { Ref } from 'vue-demi'
import { type Ref } from 'vue-demi'
import type { ConfigurableFlushSync } from '../utils'
import type { WatchPausableReturn } from '../watchPausable'
import { pausableWatch } from '../watchPausable'

export interface SyncRefOptions<L, R = L> extends ConfigurableFlushSync {
type Direction = 'ltr' | 'rtl' | 'both'
type SpecificFieldPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
/**
* A = B
*/
type Equal<A, B> = A extends B ? (B extends A ? true : false) : false

/**
* A ∩ B ≠ ∅
*/
type IntersectButNotEqual<A, B> = Equal<A, B> extends true
? false
: A & B extends never
? false
: true

/**
* A ⊆ B
*/
type IncludeButNotEqual<A, B> = Equal<A, B> extends true
? false
: A extends B
? true
: false

/**
* A ∩ B = ∅
*/
type NotIntersect<A, B> = Equal<A, B> extends true
? false
: A & B extends never
? true
: false

interface Transform<L, R> {
ltr: (left: L) => R
rtl: (right: R) => L
}

export type SyncRefOptions<L, R, D extends Direction> = ConfigurableFlushSync & {
/**
* Watch deeply
*
Expand All @@ -22,36 +61,108 @@ export interface SyncRefOptions<L, R = L> extends ConfigurableFlushSync {
*
* @default 'both'
*/
direction?: 'ltr' | 'rtl' | 'both'
direction?: D

} & {
/**
* Custom transform function
*/
transform?: {
ltr?: (left: L) => R
rtl?: (right: R) => L
}
}
both: Equal<L, R> extends true
? {
transform?: SpecificFieldPartial<Transform<L, R>, 'ltr' | 'rtl'>
}
: IncludeButNotEqual<L, R> extends true
? {
transform: SpecificFieldPartial<Transform<L, R>, 'ltr'>
}
: IncludeButNotEqual<R, L> extends true
? {
transform: SpecificFieldPartial<Transform<L, R>, 'rtl'>
}
: IntersectButNotEqual<L, R> extends true
? {
transform: Transform<L, R>
}
: NotIntersect<L, R> extends true
? {
transform: Transform<L, R>
}
: never
ltr: Equal<L, R> extends true
? {
transform?: SpecificFieldPartial<Pick<Transform<L, R>, 'ltr'>, 'ltr'>
}
: IncludeButNotEqual<L, R> extends true
? {
transform: SpecificFieldPartial<Pick<Transform<L, R>, 'ltr'>, 'ltr'>
}
: IncludeButNotEqual<R, L> extends true
? {
transform: Pick<Transform<L, R>, 'ltr'>
}
: IntersectButNotEqual<L, R> extends true
? {
transform: Pick<Transform<L, R>, 'ltr'>
}
: NotIntersect<L, R> extends true
? {
transform: Pick<Transform<L, R>, 'ltr'>
}
: never
rtl: Equal<L, R> extends true
? {
transform?: SpecificFieldPartial<Pick<Transform<L, R>, 'rtl'>, 'rtl'>
}
: IncludeButNotEqual<L, R> extends true
? {
transform: Pick<Transform<L, R>, 'rtl'>
}
: IncludeButNotEqual<R, L> extends true
? {
transform: SpecificFieldPartial<Pick<Transform<L, R>, 'rtl'>, 'rtl'>
}
: IntersectButNotEqual<L, R> extends true
? {
transform: Pick<Transform<L, R>, 'rtl'>
}
: NotIntersect<L, R> extends true
? {
transform: Pick<Transform<L, R>, 'rtl'>
}
: never
}[D]

/**
* Two-way refs synchronization.
*
* From the set theory perspective to restrict the option's type
* Check in the following order:
* 1. A = B
* 2. A ∩ B ≠ ∅
* 3. A ⊆ B
* 4. A ∩ B = ∅
* @param left
* @param right
* @param args
*/
export function syncRef<L, R = L>(left: Ref<L>, right: Ref<R>, options: SyncRefOptions<L, R> = {}) {
export function syncRef<L, R, D extends Direction = 'both'>(
left: Ref<L>,
right: Ref<R>,
...args: Equal<L, R> extends true
? [SyncRefOptions<L, R, D>?]
: [SyncRefOptions<L, R, D>]
) {
const {
flush = 'sync',
deep = false,
immediate = true,
direction = 'both',
transform = {},
} = options
} = args[0] || {}

const watchers: WatchPausableReturn[] = []

const transformLTR = transform.ltr ?? (v => v)
const transformRTL = transform.rtl ?? (v => v)
const transformLTR = ('ltr' in transform && transform.ltr) || (v => v)
const transformRTL = ('rtl' in transform && transform.rtl) || (v => v)

if (direction === 'both' || direction === 'ltr') {
watchers.push(pausableWatch(
Expand Down

0 comments on commit 8495e56

Please sign in to comment.