Skip to content

Commit e20ddb0

Browse files
authored
fix(types): allow customRef to have different getter/setter types (#14639)
The customRef function now supports two type parameters <T, S> like Ref<T, S>, allowing the getter to return type T while the setter accepts type S. Previously, CustomRefFactory only accepted a single type parameter T, which was used for both getter and setter. This prevented use cases where the setter should accept a different type than what the getter returns (e.g., parsing or transformation scenarios).
1 parent 219d83b commit e20ddb0

File tree

2 files changed

+31
-9
lines changed

2 files changed

+31
-9
lines changed

packages-private/dts-test/ref.test-d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type ToRefs,
99
type WritableComputedRef,
1010
computed,
11+
customRef,
1112
isRef,
1213
proxyRefs,
1314
reactive,
@@ -548,3 +549,22 @@ expectType<TemplateRef>(tRef)
548549

549550
const tRef2 = useTemplateRef<HTMLElement>('bar')
550551
expectType<TemplateRef<HTMLElement>>(tRef2)
552+
553+
// #14637 customRef with different getter/setter types
554+
describe('customRef with different getter/setter types', () => {
555+
// customRef should support different getter/setter types like Ref<T, S>
556+
const cr = customRef<string, number>((track, trigger) => ({
557+
get: () => 'hello',
558+
set: (val: number) => {
559+
// setter accepts number, getter returns string
560+
trigger()
561+
},
562+
}))
563+
564+
// getter returns string
565+
expectType<string>(cr.value)
566+
// setter accepts number
567+
cr.value = 123
568+
// @ts-expect-error setter doesn't accept string
569+
cr.value = 'world'
570+
})

packages/reactivity/src/ref.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -285,36 +285,36 @@ export function proxyRefs<T extends object>(
285285
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
286286
}
287287

288-
export type CustomRefFactory<T> = (
288+
export type CustomRefFactory<T, S = T> = (
289289
track: () => void,
290290
trigger: () => void,
291291
) => {
292292
get: () => T
293-
set: (value: T) => void
293+
set: (value: S) => void
294294
}
295295

296-
class CustomRefImpl<T> {
296+
class CustomRefImpl<T, S = T> {
297297
public dep: Dep
298298

299-
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
300-
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
299+
private readonly _get: ReturnType<CustomRefFactory<T, S>>['get']
300+
private readonly _set: ReturnType<CustomRefFactory<T, S>>['set']
301301

302302
public readonly [ReactiveFlags.IS_REF] = true
303303

304304
public _value: T = undefined!
305305

306-
constructor(factory: CustomRefFactory<T>) {
306+
constructor(factory: CustomRefFactory<T, S>) {
307307
const dep = (this.dep = new Dep())
308308
const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
309309
this._get = get
310310
this._set = set
311311
}
312312

313-
get value() {
313+
get value(): T {
314314
return (this._value = this._get())
315315
}
316316

317-
set value(newVal) {
317+
set value(newVal: S) {
318318
this._set(newVal)
319319
}
320320
}
@@ -326,7 +326,9 @@ class CustomRefImpl<T> {
326326
* @param factory - The function that receives the `track` and `trigger` callbacks.
327327
* @see {@link https://vuejs.org/api/reactivity-advanced.html#customref}
328328
*/
329-
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
329+
export function customRef<T, S = T>(
330+
factory: CustomRefFactory<T, S>,
331+
): Ref<T, S> {
330332
return new CustomRefImpl(factory) as any
331333
}
332334

0 commit comments

Comments
 (0)