Skip to content

Commit

Permalink
feat(syncRef): enhance syncRef type restrict (#3515)
Browse files Browse the repository at this point in the history
  • Loading branch information
Doctor-wu committed Nov 9, 2023
1 parent 8eb0b2d commit 892666b
Show file tree
Hide file tree
Showing 2 changed files with 270 additions and 16 deletions.
153 changes: 153 additions & 0 deletions packages/shared/syncRef/index.test.ts
Expand Up @@ -97,4 +97,157 @@ 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)()

syncRef(ref0, refNumBoolean, {
direction: 'ltr',
})()

syncRef(refNumBoolean, ref0, {
direction: 'rtl',
})
})
})
133 changes: 117 additions & 16 deletions packages/shared/syncRef/index.ts
@@ -1,9 +1,105 @@
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> = Partial<Pick<T, K>> & Omit<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

// L = R
interface EqualType<
D extends Direction,
L,
R,
O extends keyof Transform<L, R> = D extends 'both' ? 'ltr' | 'rtl' : D,
> {
transform?: SpecificFieldPartial<Pick<Transform<L, R>, O>, O>
}

type StrictIncludeMap<IncludeType extends 'LR' | 'RL', D extends Exclude<Direction, 'both'>, L, R> = (Equal<[IncludeType, D], ['LR', 'ltr']>
& Equal<[IncludeType, D], ['RL', 'rtl']>) extends true
? {
transform?: SpecificFieldPartial<Pick<Transform<L, R>, D>, D>
} : {
transform: Pick<Transform<L, R>, D>
}

// L ⊆ R
type StrictIncludeType<IncludeType extends 'LR' | 'RL', D extends Direction, L, R> = D extends 'both'
? {
transform: SpecificFieldPartial<Transform<L, R>, IncludeType extends 'LR' ? 'ltr' : 'rtl'>
}
: D extends Exclude<Direction, 'both'>
? StrictIncludeMap<IncludeType, D, L, R>
: never

// L ∩ R ≠ ∅
type IntersectButNotEqualType<D extends Direction, L, R> = D extends 'both'
? {
transform: Transform<L, R>
}
: D extends Exclude<Direction, 'both'>
? {
transform: Pick<Transform<L, R>, D>
}
: never

// L ∩ R = ∅
type NotIntersectType<D extends Direction, L, R> = IntersectButNotEqualType<D, L, R>
interface Transform<L, R> {
ltr: (left: L) => R
rtl: (right: R) => L
}

type TransformType<D extends Direction, L, R> = Equal<L, R> extends true
// L = R
? EqualType<D, L, R>
: IncludeButNotEqual<L, R> extends true
// L ⊆ R
? StrictIncludeType<'LR', D, L, R>
: IncludeButNotEqual<R, L> extends true
// R ⊆ L
? StrictIncludeType<'RL', D, L, R>
: IntersectButNotEqual<L, R> extends true
// L ∩ R ≠ ∅
? IntersectButNotEqualType<D, L, R>
: NotIntersect<L, R> extends true
// L ∩ R = ∅
? NotIntersectType<D, L, R>
: never

export type SyncRefOptions<L, R, D extends Direction> = ConfigurableFlushSync & {
/**
* Watch deeply
*
Expand All @@ -22,36 +118,41 @@ 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
}
}
} & TransformType<D, L, R>

/**
* Two-way refs synchronization.
*
* From the set theory perspective to restrict the option's type
* Check in the following order:
* 1. L = R
* 2. L ∩ R ≠ ∅
* 3. L ⊆ R
* 4. L ∩ R = ∅
* @param left
* @param right
* @param [options?]

Check warning on line 135 in packages/shared/syncRef/index.ts

View workflow job for this annotation

GitHub Actions / lint

Expected @param names to be "left, right, [object Object]". Got "left, right, options?"
*/
export function syncRef<L, R = L>(left: Ref<L>, right: Ref<R>, options: SyncRefOptions<L, R> = {}) {
export function syncRef<L, R, D extends Direction>(
left: Ref<L>,
right: Ref<R>,
...[options]: Equal<L, R> extends true
? [options?: SyncRefOptions<L, R, D>]
: [options: SyncRefOptions<L, R, D>]
) {
const {
flush = 'sync',
deep = false,
immediate = true,
direction = 'both',
transform = {},
} = options
} = options || {}

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 892666b

Please sign in to comment.