Skip to content

Commit

Permalink
feat: add type-level readonly() api (#593)
Browse files Browse the repository at this point in the history
* feat: add type-level `readonly()` api

* Update src/reactivity/readonly.ts

* chore: update tests
  • Loading branch information
antfu committed Dec 7, 2020
1 parent a74011a commit 3b726d4
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 61 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,17 @@ app2.component('Bar', Bar) // equivalent to Vue.use('Bar', Bar)
</details>

### `readonly`

<details>
<summary>
⚠️ <code>readonly()</code> provides **only type-level** readonly check.
</summary>

`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.

</details>

### `props`

<details>
Expand All @@ -467,7 +478,6 @@ defineComponent({

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

- `readonly`
- `defineAsyncComponent`
- `onRenderTracked`
- `onRenderTriggered`
Expand Down
14 changes: 12 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ watch(

</details>

### shallowReadonly
### `shallowReadonly`

<details>
<summary>
Expand All @@ -416,6 +416,17 @@ watch(
</details>

### `readonly`

<details>
<summary>
⚠️ <code>readonly()</code> **只提供类型层面**的只读。
</summary>

`readonly()` 只在类型层面提供和 Vue 3 的对齐。在其返回值或其属性上使用 <code>isReadonly()</code> 检查的结果将无法保证。

</details>

### `props`

<details>
Expand All @@ -442,7 +453,6 @@ defineComponent({

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

- `readonly`
- `defineAsyncComponent`
- `onRenderTracked`
- `onRenderTriggered`
Expand Down
2 changes: 2 additions & 0 deletions src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export {
shallowReadonly,
proxyRefs,
ShallowUnwrapRef,
readonly,
DeepReadonly,
} from '../reactivity'
export * from './lifecycle'
export * from './watch'
Expand Down
3 changes: 1 addition & 2 deletions src/reactivity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ export {
shallowReactive,
toRaw,
isRaw,
isReadonly,
shallowReadonly,
} from './reactive'
export {
ref,
Expand All @@ -23,5 +21,6 @@ export {
proxyRefs,
ShallowUnwrapRef,
} from './ref'
export { readonly, isReadonly, shallowReadonly, DeepReadonly } from './readonly'
export { set } from './set'
export { del } from './del'
57 changes: 1 addition & 56 deletions src/reactivity/reactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,12 @@ import { isPlainObject, def, warn, isArray, hasOwn, noopFn } from '../utils'
import { isComponentInstance, defineComponentInstance } from '../utils/helper'
import { RefKey } from '../utils/symbols'
import { isRef, UnwrapRef } from './ref'
import { rawSet, accessModifiedSet, readonlySet } from '../utils/sets'
import { rawSet, accessModifiedSet } from '../utils/sets'

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

export function isReadonly(obj: any): boolean {
return readonlySet.has(obj)
}

export function isReactive(obj: any): boolean {
return Boolean(obj?.__ob__ && !obj.__ob__?.__raw__)
}
Expand Down Expand Up @@ -219,57 +215,6 @@ export function reactive<T extends object>(obj: T): UnwrapRef<T> {
return observed as UnwrapRef<T>
}

export function shallowReadonly<T extends object>(obj: T): Readonly<T>
export function shallowReadonly(obj: any): any {
if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
return obj
}

const readonlyObj = {}

const source = reactive({})
const ob = (source as any).__ob__

for (const key of Object.keys(obj)) {
let val = obj[key]
let getter: (() => any) | undefined
let setter: ((x: any) => void) | undefined
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property) {
if (property.configurable === false) {
continue
}
getter = property.get
setter = property.set
if (
(!getter || setter) /* not only have getter */ &&
arguments.length === 2
) {
val = obj[key]
}
}

Object.defineProperty(readonlyObj, key, {
enumerable: true,
configurable: true,
get: function getterHandler() {
const value = getter ? getter.call(obj) : val
ob.dep.depend()
return value
},
set(v) {
if (__DEV__) {
warn(`Set operation on key "${key}" failed: target is readonly.`)
}
},
})
}

readonlySet.set(readonlyObj, true)

return readonlyObj
}

/**
* Make sure obj can't be a reactive
*/
Expand Down
97 changes: 97 additions & 0 deletions src/reactivity/readonly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { reactive, Ref, UnwrapRef } from '.'
import { isArray, isPlainObject, warn } from '../utils'
import { readonlySet } from '../utils/sets'

export function isReadonly(obj: any): boolean {
return readonlySet.has(obj)
}

type Primitive = string | number | boolean | bigint | symbol | undefined | null
type Builtin = Primitive | Function | Date | Error | RegExp

// prettier-ignore
export type DeepReadonly<T> = T extends Builtin
? T
: T extends Map<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends ReadonlyMap<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends WeakMap<infer K, infer V>
? WeakMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends Set<infer U>
? ReadonlySet<DeepReadonly<U>>
: T extends ReadonlySet<infer U>
? ReadonlySet<DeepReadonly<U>>
: T extends WeakSet<infer U>
? WeakSet<DeepReadonly<U>>
: T extends Promise<infer U>
? Promise<DeepReadonly<U>>
: T extends {}
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: Readonly<T>

// only unwrap nested ref
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

/**
* **In @vue/composition-api, `reactive` only provides type-level readonly check**
*
* Creates a readonly copy of the original object. Note the returned copy is not
* made reactive, but `readonly` can be called on an already reactive object.
*/
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return target as any
}

export function shallowReadonly<T extends object>(obj: T): Readonly<T>
export function shallowReadonly(obj: any): any {
if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
return obj
}

const readonlyObj = {}

const source = reactive({})
const ob = (source as any).__ob__

for (const key of Object.keys(obj)) {
let val = obj[key]
let getter: (() => any) | undefined
let setter: ((x: any) => void) | undefined
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property) {
if (property.configurable === false) {
continue
}
getter = property.get
setter = property.set
if (
(!getter || setter) /* not only have getter */ &&
arguments.length === 2
) {
val = obj[key]
}
}

Object.defineProperty(readonlyObj, key, {
enumerable: true,
configurable: true,
get: function getterHandler() {
const value = getter ? getter.call(obj) : val
ob.dep.depend()
return value
},
set(v) {
if (__DEV__) {
warn(`Set operation on key "${key}" failed: target is readonly.`)
}
},
})
}

readonlySet.set(readonlyObj, true)

return readonlyObj
}
48 changes: 48 additions & 0 deletions test-dts/readonly.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expectType, readonly, ref } from './index'

describe('readonly', () => {
it('nested', () => {
const r = readonly({
obj: { k: 'v' },
arr: [1, 2, '3'],
objInArr: [{ foo: 'bar' }],
})

// @ts-expect-error
r.obj = {}
// @ts-expect-error
r.obj.k = 'x'

// @ts-expect-error
r.arr.push(42)
// @ts-expect-error
r.objInArr[0].foo = 'bar2'
})

it('with ref', () => {
const r = readonly(
ref({
obj: { k: 'v' },
arr: [1, 2, '3'],
objInArr: [{ foo: 'bar' }],
})
)

console.log(r.value)

expectType<string>(r.value.obj.k)

// @ts-expect-error
r.value = {}

// @ts-expect-error
r.value.obj = {}
// @ts-expect-error
r.value.obj.k = 'x'

// @ts-expect-error
r.value.arr.push(42)
// @ts-expect-error
r.value.objInArr[0].foo = 'bar2'
})
})

0 comments on commit 3b726d4

Please sign in to comment.