Repository: vuejs/composition-api
Pull request #593 (2020-11-22 18:19:49)

feat: add type-level `readonly()` api


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7de542..c0d478d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+<a name="1.0.0-beta.20"></a>
+# [1.0.0-beta.20](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.19...v1.0.0-beta.20) (2020-11-24)
+
+
+### Bug Fixes
+
+* **types:** improve SetupContext types ([#595](https://github.com/vuejs/composition-api/issues/595)) ([49766bf](https://github.com/vuejs/composition-api/commit/49766bf))
+
+
+### Features
+
+* add warn ([#596](https://github.com/vuejs/composition-api/issues/596)) ([dd2cd6b](https://github.com/vuejs/composition-api/commit/dd2cd6b))
+
+
+
 <a name="1.0.0-beta.19"></a>
 # [1.0.0-beta.19](https://github.com/vuejs/composition-api/compare/v1.0.0-beta.18...v1.0.0-beta.19) (2020-11-02)
 
diff --git a/README.md b/README.md
index ae736fa..0a1072c 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Vue 2 plugin for **Composition API**
 [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/vuejs/composition-api/Build%20&%20Test)](https://github.com/vuejs/composition-api/actions?query=workflow%3A%22Build+%26+Test%22)
 [![Minzipped size](https://badgen.net/bundlephobia/minzip/@vue/composition-api)](https://bundlephobia.com/result?p=@vue/composition-api)
 
-English | [中文](./README.zh-CN.md) ・ [**Composition API Docs**](https://composition-api.vuejs.org/)
+English | [中文](./README.zh-CN.md) ・ [**Composition API Docs**](https://v3.vuejs.org/guide/composition-api-introduction.html)
 
 ## Installation
 
@@ -41,7 +41,7 @@ Include `@vue/composition-api` after Vue and it will install itself automaticall
 <!--cdn-links-start-->
 ```html
 <script src="https://cdn.jsdelivr.net/npm/vue@2.6"></script>
-<script src="https://cdn.jsdelivr.net/npm/@vue/composition-api@1.0.0-beta.19"></script>
+<script src="https://cdn.jsdelivr.net/npm/@vue/composition-api@1.0.0-beta.20"></script>
 ```
 <!--cdn-links-end-->
 
@@ -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>
@@ -467,7 +478,6 @@ defineComponent({
 
 The following APIs introduced in Vue 3 are not available in this plugin.
 
-- `readonly`
 - `defineAsyncComponent`
 - `onRenderTracked`
 - `onRenderTriggered`
@@ -520,6 +530,6 @@ defineComponent({
 
 ### Performance Impact
 
-Due the the limitation of Vue2's public API. `@vue/composition-api` inevitably introduced some extract costs. It shouldn't bother you unless in extreme environments.
+Due the the limitation of Vue2's public API. `@vue/composition-api` inevitably introduces some performance overhead. Note that in most scenarios, this shouldn't be the source of performance issues.
 
 You can check the [benchmark results](https://antfu.github.io/vue-composition-api-benchmark-results/) for more details.
diff --git a/README.zh-CN.md b/README.zh-CN.md
index f94bb80..ae9a04b 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -5,7 +5,7 @@
 [![npm](https://img.shields.io/npm/v/@vue/composition-api)](https://www.npmjs.com/package/@vue/composition-api)
 [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/vuejs/composition-api/Build%20&%20Test)](https://github.com/vuejs/composition-api/actions?query=workflow%3A%22Build+%26+Test%22)
 
-[English](./README.md) | 中文 ・ [**组合式 API 文档**](https://composition-api.vuejs.org/zh)
+[English](./README.md) | 中文 ・ [**组合式 API 文档**](https://v3.cn.vuejs.org/guide/composition-api-introduction.html)
 
 ## 安装
 
@@ -40,7 +40,7 @@ import { ref, reactive } from '@vue/composition-api'
 <!--cdn-links-start-->
 ```html
 <script src="https://cdn.jsdelivr.net/npm/vue@2.6"></script>
-<script src="https://cdn.jsdelivr.net/npm/@vue/composition-api@1.0.0-beta.19"></script>
+<script src="https://cdn.jsdelivr.net/npm/@vue/composition-api@1.0.0-beta.20"></script>
 ```
 <!--cdn-links-end-->
 
@@ -405,7 +405,7 @@ watch(
 
 </details>
 
-### shallowReadonly
+### `shallowReadonly`
 
 <details>
 <summary>
@@ -416,6 +416,17 @@ watch(
 
 </details>
 
+### `readonly`
+
+<details>
+<summary>
+⚠️ <code>readonly()</code> **只提供类型层面**的只读。
+</summary>
+
+`readonly()` 只在类型层面提供和 Vue 3 的对齐。在其返回值或其属性上使用 <code>isReadonly()</code> 检查的结果将无法保证。
+
+</details>
+
 ### `props`
 
 <details>
@@ -442,7 +453,6 @@ defineComponent({
 
 以下在 Vue 3 新引入的 API ，在本插件中暂不适用：
 
-- `readonly`
 - `defineAsyncComponent`
 - `onRenderTracked`
 - `onRenderTriggered`
diff --git a/package.json b/package.json
index 49e232b..431cbaa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@vue/composition-api",
-  "version": "1.0.0-beta.19",
+  "version": "1.0.0-beta.20",
   "description": "Provide logic composition capabilities for Vue.",
   "keywords": [
     "vue",
diff --git a/src/apis/index.ts b/src/apis/index.ts
index bf2e31e..ae33a86 100644
--- a/src/apis/index.ts
+++ b/src/apis/index.ts
@@ -21,6 +21,8 @@ export {
   shallowReadonly,
   proxyRefs,
   ShallowUnwrapRef,
+  readonly,
+  DeepReadonly,
 } from '../reactivity'
 export * from './lifecycle'
 export * from './watch'
@@ -30,3 +32,4 @@ export { useCSSModule } from './useCssModule'
 export { createApp } from './createApp'
 export { nextTick } from './nextTick'
 export { createElement as h } from './createElement'
+export { warn } from './warn'
diff --git a/src/apis/warn.ts b/src/apis/warn.ts
new file mode 100644
index 0000000..2bb5236
--- /dev/null
+++ b/src/apis/warn.ts
@@ -0,0 +1,12 @@
+import { getCurrentInstance } from '../runtimeContext'
+import { warn as vueWarn } from '../utils'
+
+/**
+ * Displays a warning message (using console.error) with a stack trace if the
+ * function is called inside of active component.
+ *
+ * @param message warning message to be displayed
+ */
+export function warn(message: string) {
+  vueWarn(message, getCurrentInstance())
+}
diff --git a/src/component/componentOptions.ts b/src/component/componentOptions.ts
index 5f0f150..c78decf 100644
--- a/src/component/componentOptions.ts
+++ b/src/component/componentOptions.ts
@@ -5,10 +5,27 @@ import { ComponentInstance, ComponentRenderProxy } from './componentProxy'
 
 export interface SetupContext {
   readonly attrs: Data
-  readonly slots: { [key: string]: (...args: any[]) => VNode[] }
+  readonly slots: Readonly<{ [key in string]?: (...args: any[]) => VNode[] }>
+
+  /**
+   * @deprecated not avaliable in Vue 3
+   */
   readonly parent: ComponentInstance | null
+
+  /**
+   * @deprecated not avaliable in Vue 3
+   */
   readonly root: ComponentInstance
-  readonly listeners: { [key: string]: Function }
+
+  /**
+   * @deprecated not avaliable in Vue 3
+   */
+  readonly listeners: { [key in string]?: Function }
+
+  /**
+   * @deprecated not avaliable in Vue 3
+   */
+  readonly refs: Data
 
   emit(event: string, ...args: any[]): void
 }
diff --git a/src/component/componentProxy.ts b/src/component/componentProxy.ts
index 82fb044..d614eaf 100644
--- a/src/component/componentProxy.ts
+++ b/src/component/componentProxy.ts
@@ -41,13 +41,19 @@ export type ComponentRenderProxy<
   Omit<Vue, '$data' | '$props' | '$attrs'>
 
 // for Vetur and TSX support
-type VueConstructorProxy<PropsOptions, RawBindings> = VueConstructor & {
+type VueConstructorProxy<
+  PropsOptions,
+  RawBindings,
+  Data,
+  Computed extends ComputedOptions,
+  Methods extends MethodOptions
+> = VueConstructor & {
   new (...args: any[]): ComponentRenderProxy<
     ExtractPropTypes<PropsOptions>,
     ShallowUnwrapRef<RawBindings>,
-    ExtractPropTypes<PropsOptions>,
-    {},
-    {},
+    Data,
+    Computed,
+    Methods,
     ExtractPropTypes<PropsOptions>,
     ExtractDefaultPropTypes<PropsOptions>,
     true
@@ -62,8 +68,8 @@ export type VueProxy<
   PropsOptions,
   RawBindings,
   Data = DefaultData<Vue>,
-  Computed = DefaultComputed,
-  Methods = DefaultMethods<Vue>
+  Computed extends ComputedOptions = DefaultComputed,
+  Methods extends MethodOptions = DefaultMethods<Vue>
 > = Vue2ComponentOptions<
   Vue,
   ShallowUnwrapRef<RawBindings> & Data,
@@ -72,4 +78,4 @@ export type VueProxy<
   PropsOptions,
   ExtractPropTypes<PropsOptions>
 > &
-  VueConstructorProxy<PropsOptions, RawBindings>
+  VueConstructorProxy<PropsOptions, RawBindings, Data, Computed, Methods>
diff --git a/src/env.d.ts b/src/env.d.ts
index 161e9a6..8e36248 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -19,7 +19,7 @@ declare module 'vue/types/vue' {
   interface VueConstructor {
     observable<T>(x: any): T
     util: {
-      warn(msg: string, vm?: Vue)
+      warn(msg: string, vm?: Vue | null)
       defineReactive(
         obj: Object,
         key: string,
diff --git a/src/mixin.ts b/src/mixin.ts
index 17fe2e8..0e67e4b 100644
--- a/src/mixin.ts
+++ b/src/mixin.ts
@@ -88,6 +88,7 @@ export function mixin(Vue: VueConstructor) {
     def(props, '__ob__', createObserver())
 
     // resolve scopedSlots and slots to functions
+    // @ts-expect-error
     resolveScopedSlots(vm, ctx.slots)
 
     let binding: ReturnType<SetupFunction<Data, Data>> | undefined | null
@@ -102,6 +103,7 @@ export function mixin(Vue: VueConstructor) {
       const bindingFunc = binding
       // keep currentInstance accessible for createElement
       vm.$options.render = () => {
+        // @ts-expect-error
         resolveScopedSlots(vm, ctx.slots)
         return activateCurrentInstance(vm, () => bindingFunc())
       }
diff --git a/src/reactivity/index.ts b/src/reactivity/index.ts
index f674197..b045e91 100644
--- a/src/reactivity/index.ts
+++ b/src/reactivity/index.ts
@@ -5,8 +5,6 @@ export {
   shallowReactive,
   toRaw,
   isRaw,
-  isReadonly,
-  shallowReadonly,
 } from './reactive'
 export {
   ref,
@@ -23,5 +21,6 @@ export {
   proxyRefs,
   ShallowUnwrapRef,
 } from './ref'
+export { readonly, isReadonly, shallowReadonly, DeepReadonly } from './readonly'
 export { set } from './set'
 export { del } from './del'
diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts
index e594e53..9414051 100644
--- a/src/reactivity/reactive.ts
+++ b/src/reactivity/reactive.ts
@@ -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__)
 }
@@ -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
  */
diff --git a/src/reactivity/readonly.ts b/src/reactivity/readonly.ts
new file mode 100644
index 0000000..10ca241
--- /dev/null
+++ b/src/reactivity/readonly.ts
@@ -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
+}
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 0bffd6d..2c4f4fc 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -83,7 +83,7 @@ export function isUndef(v: any): boolean {
   return v === undefined || v === null
 }
 
-export function warn(msg: string, vm?: Vue) {
+export function warn(msg: string, vm?: Vue | null) {
   Vue.util.warn(msg, vm)
 }
 


Test patch:
diff --git a/test-dts/defineComponent.test-d.ts b/test-dts/defineComponent.test-d.ts
index bf5ddef..cd0b56a 100644
--- a/test-dts/defineComponent.test-d.ts
+++ b/test-dts/defineComponent.test-d.ts
@@ -667,3 +667,41 @@ describe('emits', () => {
   })
   */
 })
+
+describe('vetur', () => {
+  // #609
+  it('should have access to options API', () => {
+    const Comp = defineComponent({
+      data() {
+        return {
+          a: 1,
+        }
+      },
+
+      computed: {
+        ac() {
+          return 1
+        },
+      },
+
+      methods: {
+        callA(b: number) {
+          return b
+        },
+      },
+
+      setup() {
+        return {
+          sa: '1',
+        }
+      },
+    })
+
+    const comp = new Comp()
+
+    expectType<number>(comp.a)
+    expectType<number>(comp.ac)
+    expectType<string>(comp.sa)
+    expectType<(b: number) => number>(comp.callA)
+  })
+})
diff --git a/test-dts/readonly.test-d.ts b/test-dts/readonly.test-d.ts
new file mode 100644
index 0000000..1ca26a5
--- /dev/null
+++ b/test-dts/readonly.test-d.ts
@@ -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'
+  })
+})
diff --git a/test/apis/warn.spec.js b/test/apis/warn.spec.js
new file mode 100644
index 0000000..9ceb5fa
--- /dev/null
+++ b/test/apis/warn.spec.js
@@ -0,0 +1,32 @@
+const Vue = require('vue/dist/vue.common.js')
+const { warn: apiWarn } = require('../../src')
+
+describe('api/warn', () => {
+  beforeEach(() => {
+    warn = jest.spyOn(global.console, 'error').mockImplementation(() => null)
+  })
+  afterEach(() => {
+    warn.mockRestore()
+  })
+
+  it('can be called inside a component', () => {
+    new Vue({
+      setup() {
+        apiWarn('warned')
+      },
+      template: `<div></div>`,
+    }).$mount()
+
+    expect(warn).toHaveBeenCalledTimes(1)
+    expect(warn.mock.calls[0][0]).toMatch(
+      /\[Vue warn\]: warned[\s\S]*\(found in <Root>\)/
+    )
+  })
+
+  it('can be called outside a component', () => {
+    apiWarn('warned')
+
+    expect(warn).toHaveBeenCalledTimes(1)
+    expect(warn).toHaveBeenCalledWith('[Vue warn]: warned')
+  })
+})