Skip to content

Commit 3b726d4

Browse files
authored
feat: add type-level readonly() api (#593)
* feat: add type-level `readonly()` api * Update src/reactivity/readonly.ts * chore: update tests
1 parent a74011a commit 3b726d4

File tree

7 files changed

+172
-61
lines changed

7 files changed

+172
-61
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,17 @@ app2.component('Bar', Bar) // equivalent to Vue.use('Bar', Bar)
441441
442442
</details>
443443

444+
### `readonly`
445+
446+
<details>
447+
<summary>
448+
⚠️ <code>readonly()</code> provides **only type-level** readonly check.
449+
</summary>
450+
451+
`readonly()` is provided as API alignment with Vue 3 on type-level only. Use <code>isReadonly()</code> on it or it's properties can not be guaranteed.
452+
453+
</details>
454+
444455
### `props`
445456

446457
<details>
@@ -467,7 +478,6 @@ defineComponent({
467478

468479
The following APIs introduced in Vue 3 are not available in this plugin.
469480

470-
- `readonly`
471481
- `defineAsyncComponent`
472482
- `onRenderTracked`
473483
- `onRenderTriggered`

README.zh-CN.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ watch(
405405

406406
</details>
407407

408-
### shallowReadonly
408+
### `shallowReadonly`
409409

410410
<details>
411411
<summary>
@@ -416,6 +416,17 @@ watch(
416416
417417
</details>
418418

419+
### `readonly`
420+
421+
<details>
422+
<summary>
423+
⚠️ <code>readonly()</code> **只提供类型层面**的只读。
424+
</summary>
425+
426+
`readonly()` 只在类型层面提供和 Vue 3 的对齐。在其返回值或其属性上使用 <code>isReadonly()</code> 检查的结果将无法保证。
427+
428+
</details>
429+
419430
### `props`
420431

421432
<details>
@@ -442,7 +453,6 @@ defineComponent({
442453

443454
以下在 Vue 3 新引入的 API ,在本插件中暂不适用:
444455

445-
- `readonly`
446456
- `defineAsyncComponent`
447457
- `onRenderTracked`
448458
- `onRenderTriggered`

src/apis/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export {
2121
shallowReadonly,
2222
proxyRefs,
2323
ShallowUnwrapRef,
24+
readonly,
25+
DeepReadonly,
2426
} from '../reactivity'
2527
export * from './lifecycle'
2628
export * from './watch'

src/reactivity/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ export {
55
shallowReactive,
66
toRaw,
77
isRaw,
8-
isReadonly,
9-
shallowReadonly,
108
} from './reactive'
119
export {
1210
ref,
@@ -23,5 +21,6 @@ export {
2321
proxyRefs,
2422
ShallowUnwrapRef,
2523
} from './ref'
24+
export { readonly, isReadonly, shallowReadonly, DeepReadonly } from './readonly'
2625
export { set } from './set'
2726
export { del } from './del'

src/reactivity/reactive.ts

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@ import { isPlainObject, def, warn, isArray, hasOwn, noopFn } from '../utils'
44
import { isComponentInstance, defineComponentInstance } from '../utils/helper'
55
import { RefKey } from '../utils/symbols'
66
import { isRef, UnwrapRef } from './ref'
7-
import { rawSet, accessModifiedSet, readonlySet } from '../utils/sets'
7+
import { rawSet, accessModifiedSet } from '../utils/sets'
88

99
export function isRaw(obj: any): boolean {
1010
return Boolean(obj?.__ob__ && obj.__ob__?.__raw__)
1111
}
1212

13-
export function isReadonly(obj: any): boolean {
14-
return readonlySet.has(obj)
15-
}
16-
1713
export function isReactive(obj: any): boolean {
1814
return Boolean(obj?.__ob__ && !obj.__ob__?.__raw__)
1915
}
@@ -219,57 +215,6 @@ export function reactive<T extends object>(obj: T): UnwrapRef<T> {
219215
return observed as UnwrapRef<T>
220216
}
221217

222-
export function shallowReadonly<T extends object>(obj: T): Readonly<T>
223-
export function shallowReadonly(obj: any): any {
224-
if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
225-
return obj
226-
}
227-
228-
const readonlyObj = {}
229-
230-
const source = reactive({})
231-
const ob = (source as any).__ob__
232-
233-
for (const key of Object.keys(obj)) {
234-
let val = obj[key]
235-
let getter: (() => any) | undefined
236-
let setter: ((x: any) => void) | undefined
237-
const property = Object.getOwnPropertyDescriptor(obj, key)
238-
if (property) {
239-
if (property.configurable === false) {
240-
continue
241-
}
242-
getter = property.get
243-
setter = property.set
244-
if (
245-
(!getter || setter) /* not only have getter */ &&
246-
arguments.length === 2
247-
) {
248-
val = obj[key]
249-
}
250-
}
251-
252-
Object.defineProperty(readonlyObj, key, {
253-
enumerable: true,
254-
configurable: true,
255-
get: function getterHandler() {
256-
const value = getter ? getter.call(obj) : val
257-
ob.dep.depend()
258-
return value
259-
},
260-
set(v) {
261-
if (__DEV__) {
262-
warn(`Set operation on key "${key}" failed: target is readonly.`)
263-
}
264-
},
265-
})
266-
}
267-
268-
readonlySet.set(readonlyObj, true)
269-
270-
return readonlyObj
271-
}
272-
273218
/**
274219
* Make sure obj can't be a reactive
275220
*/

src/reactivity/readonly.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { reactive, Ref, UnwrapRef } from '.'
2+
import { isArray, isPlainObject, warn } from '../utils'
3+
import { readonlySet } from '../utils/sets'
4+
5+
export function isReadonly(obj: any): boolean {
6+
return readonlySet.has(obj)
7+
}
8+
9+
type Primitive = string | number | boolean | bigint | symbol | undefined | null
10+
type Builtin = Primitive | Function | Date | Error | RegExp
11+
12+
// prettier-ignore
13+
export type DeepReadonly<T> = T extends Builtin
14+
? T
15+
: T extends Map<infer K, infer V>
16+
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
17+
: T extends ReadonlyMap<infer K, infer V>
18+
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
19+
: T extends WeakMap<infer K, infer V>
20+
? WeakMap<DeepReadonly<K>, DeepReadonly<V>>
21+
: T extends Set<infer U>
22+
? ReadonlySet<DeepReadonly<U>>
23+
: T extends ReadonlySet<infer U>
24+
? ReadonlySet<DeepReadonly<U>>
25+
: T extends WeakSet<infer U>
26+
? WeakSet<DeepReadonly<U>>
27+
: T extends Promise<infer U>
28+
? Promise<DeepReadonly<U>>
29+
: T extends {}
30+
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
31+
: Readonly<T>
32+
33+
// only unwrap nested ref
34+
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
35+
36+
/**
37+
* **In @vue/composition-api, `reactive` only provides type-level readonly check**
38+
*
39+
* Creates a readonly copy of the original object. Note the returned copy is not
40+
* made reactive, but `readonly` can be called on an already reactive object.
41+
*/
42+
export function readonly<T extends object>(
43+
target: T
44+
): DeepReadonly<UnwrapNestedRefs<T>> {
45+
return target as any
46+
}
47+
48+
export function shallowReadonly<T extends object>(obj: T): Readonly<T>
49+
export function shallowReadonly(obj: any): any {
50+
if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
51+
return obj
52+
}
53+
54+
const readonlyObj = {}
55+
56+
const source = reactive({})
57+
const ob = (source as any).__ob__
58+
59+
for (const key of Object.keys(obj)) {
60+
let val = obj[key]
61+
let getter: (() => any) | undefined
62+
let setter: ((x: any) => void) | undefined
63+
const property = Object.getOwnPropertyDescriptor(obj, key)
64+
if (property) {
65+
if (property.configurable === false) {
66+
continue
67+
}
68+
getter = property.get
69+
setter = property.set
70+
if (
71+
(!getter || setter) /* not only have getter */ &&
72+
arguments.length === 2
73+
) {
74+
val = obj[key]
75+
}
76+
}
77+
78+
Object.defineProperty(readonlyObj, key, {
79+
enumerable: true,
80+
configurable: true,
81+
get: function getterHandler() {
82+
const value = getter ? getter.call(obj) : val
83+
ob.dep.depend()
84+
return value
85+
},
86+
set(v) {
87+
if (__DEV__) {
88+
warn(`Set operation on key "${key}" failed: target is readonly.`)
89+
}
90+
},
91+
})
92+
}
93+
94+
readonlySet.set(readonlyObj, true)
95+
96+
return readonlyObj
97+
}

test-dts/readonly.test-d.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { expectType, readonly, ref } from './index'
2+
3+
describe('readonly', () => {
4+
it('nested', () => {
5+
const r = readonly({
6+
obj: { k: 'v' },
7+
arr: [1, 2, '3'],
8+
objInArr: [{ foo: 'bar' }],
9+
})
10+
11+
// @ts-expect-error
12+
r.obj = {}
13+
// @ts-expect-error
14+
r.obj.k = 'x'
15+
16+
// @ts-expect-error
17+
r.arr.push(42)
18+
// @ts-expect-error
19+
r.objInArr[0].foo = 'bar2'
20+
})
21+
22+
it('with ref', () => {
23+
const r = readonly(
24+
ref({
25+
obj: { k: 'v' },
26+
arr: [1, 2, '3'],
27+
objInArr: [{ foo: 'bar' }],
28+
})
29+
)
30+
31+
console.log(r.value)
32+
33+
expectType<string>(r.value.obj.k)
34+
35+
// @ts-expect-error
36+
r.value = {}
37+
38+
// @ts-expect-error
39+
r.value.obj = {}
40+
// @ts-expect-error
41+
r.value.obj.k = 'x'
42+
43+
// @ts-expect-error
44+
r.value.arr.push(42)
45+
// @ts-expect-error
46+
r.value.objInArr[0].foo = 'bar2'
47+
})
48+
})

0 commit comments

Comments
 (0)