Repository: vuejs/composition-api (ts)

Issue:
attrs is not reactive when destructured
Minimal (non-)working example:
```vue
<template>
  <div>
    {{ $attrs['hello-world'] }}
    {{ myComputed }}
  </div>
</template>

<script>
import { defineComponent, computed } from "@vue/composition-api";

export default defineComponent({
  inheritAttrs: false,

  setup(props, { attrs }) {
    const myComputed = computed(() => attrs['hello-world']);

    return {
      myComputed
    }
  }
});
</script>
```

```vue
<template>
    <Example :hello-world="test"/>
   <button @click="change">Change me</button>
</template>

<script>
import { defineComponent, ref } from "@vue/composition-api";

import Example from "@/components/Example.vue";

export default defineComponent({
  components: { Example },

  setup() {
    const test = ref("Hey");

    return {
      change() {
        test.value = "What?";
      },
      test
    }
  }
});
</script>
```

The value in the template, `$attrs` changes accordingly, but the computed value doesn't.

Pull request:
fix: destructure `attrs` from context keep reactive, close #264

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..b305193 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-->
 
@@ -112,6 +112,10 @@ export default {
 }
 ```
 
+## Browser Compatibility
+
+`@vue/composition-api` supports all modern browsers and IE11+. For lower versions IE you should install `WeakMap` polyfill (for example from `core-js` package).
+
 ## Limitations
 
 > :white_check_mark: Support &nbsp;&nbsp;&nbsp;&nbsp;:x: Not Supported
@@ -441,6 +445,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 +482,6 @@ defineComponent({
 
 The following APIs introduced in Vue 3 are not available in this plugin.
 
-- `readonly`
 - `defineAsyncComponent`
 - `onRenderTracked`
 - `onRenderTriggered`
@@ -520,6 +534,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..4216bb3 100644
--- a/src/mixin.ts
+++ b/src/mixin.ts
@@ -25,7 +25,7 @@ import {
   asVmProperty,
 } from './utils/instance'
 import { getVueConstructor } from './runtimeContext'
-import { createObserver } from './reactivity/reactive'
+import { createObserver, reactive } from './reactivity/reactive'
 
 export function mixin(Vue: VueConstructor) {
   Vue.mixin({
@@ -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())
       }
@@ -190,38 +192,59 @@ export function mixin(Vue: VueConstructor) {
   function createSetupContext(
     vm: ComponentInstance & { [x: string]: any }
   ): SetupContext {
-    const ctx = {
-      slots: {},
-    } as SetupContext
-    const props: Array<string | [string, string]> = [
+    const ctx = { slots: {} } as SetupContext
+
+    const propsPlain = [
       'root',
       'parent',
       'refs',
-      'attrs',
       'listeners',
       'isServer',
       'ssrContext',
     ]
+    const propsReactiveProxy = ['attrs']
     const methodReturnVoid = ['emit']
-    props.forEach((key) => {
-      let targetKey: string
-      let srcKey: string
-      if (Array.isArray(key)) {
-        ;[targetKey, srcKey] = key
-      } else {
-        targetKey = srcKey = key
-      }
-      srcKey = `$${srcKey}`
-      proxy(ctx, targetKey, {
+
+    propsPlain.forEach((key) => {
+      let srcKey = `$${key}`
+      proxy(ctx, key, {
         get: () => vm[srcKey],
         set() {
           warn(
-            `Cannot assign to '${targetKey}' because it is a read-only property`,
+            `Cannot assign to '${key}' because it is a read-only property`,
+            vm
+          )
+        },
+      })
+    })
+
+    propsReactiveProxy.forEach((key) => {
+      let srcKey = `$${key}`
+      proxy(ctx, key, {
+        get: () => {
+          const data = reactive({})
+          const source = vm[srcKey]
+
+          for (const attr of Object.keys(source)) {
+            proxy(data, attr, {
+              get: () => {
+                // to ensure it always return the latest value
+                return vm[srcKey][attr]
+              },
+            })
+          }
+
+          return data
+        },
+        set() {
+          warn(
+            `Cannot assign to '${key}' because it is a read-only property`,
             vm
           )
         },
       })
     })
+
     methodReturnVoid.forEach((key) => {
       const srcKey = `$${key}`
       proxy(ctx, key, {
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')
+  })
+})
diff --git a/test/setupContext.spec.js b/test/setupContext.spec.js
deleted file mode 100644
index 1d33cb5..0000000
--- a/test/setupContext.spec.js
+++ /dev/null
@@ -1,135 +0,0 @@
-const Vue = require('vue/dist/vue.common.js')
-const { h } = require('../src')
-
-describe('setupContext', () => {
-  it('should have proper properties', () => {
-    let context
-    const vm = new Vue({
-      setup(_, ctx) {
-        context = ctx
-      },
-    })
-    expect(context).toBeDefined()
-    expect('parent' in context).toBe(true)
-    expect(context.root).toBe(vm.$root)
-    expect(context.parent).toBe(vm.$parent)
-    expect(context.slots).toBeDefined()
-    expect(context.attrs).toBe(vm.$attrs)
-    expect(context.listeners).toBe(vm.$listeners)
-
-    // CAUTION: this will be removed in 3.0
-    expect(context.refs).toBe(vm.$refs)
-    expect(typeof context.emit === 'function').toBe(true)
-  })
-
-  it('slots should work in render function', () => {
-    const vm = new Vue({
-      template: `
-        <test>
-          <template slot="default">
-            <span>foo</span>
-          </template>
-          <template slot="item" slot-scope="props">
-            <span>{{ props.text || 'meh' }}</span>
-          </template>
-        </test>
-      `,
-      components: {
-        test: {
-          setup(_, { slots }) {
-            return () => {
-              return h('div', [slots.default(), slots.item()])
-            }
-          },
-        },
-      },
-    }).$mount()
-    expect(vm.$el.innerHTML).toBe('<span>foo</span><span>meh</span>')
-  })
-
-  it('warn for slots calls outside of the render() function', () => {
-    warn = jest.spyOn(global.console, 'error').mockImplementation(() => null)
-
-    new Vue({
-      template: `
-        <test>
-          <template slot="default">
-            <span>foo</span>
-          </template>
-        </test>
-      `,
-      components: {
-        test: {
-          setup(_, { slots }) {
-            slots.default()
-          },
-        },
-      },
-    }).$mount()
-    expect(warn.mock.calls[0][0]).toMatch(
-      'slots.default() got called outside of the "render()" scope'
-    )
-    warn.mockRestore()
-  })
-
-  it('staled slots should be removed', () => {
-    const Child = {
-      template: '<div><slot value="foo"/></div>',
-    }
-    const vm = new Vue({
-      components: { Child },
-      template: `
-        <child>
-          <template slot-scope="{ value }" v-if="value">
-            foo {{ value }}
-          </template>
-        </child>
-      `,
-    }).$mount()
-    expect(vm.$el.textContent).toMatch(`foo foo`)
-  })
-
-  it('slots should be synchronized', (done) => {
-    let slotKeys
-    const Foo = {
-      setup(_, { slots }) {
-        slotKeys = Object.keys(slots)
-        return () => {
-          slotKeys = Object.keys(slots)
-          return h('div', [
-            slots.default && slots.default('from foo default'),
-            slots.one && slots.one('from foo one'),
-            slots.two && slots.two('from foo two'),
-            slots.three && slots.three('from foo three'),
-          ])
-        }
-      },
-    }
-
-    const vm = new Vue({
-      data: {
-        a: 'one',
-        b: 'two',
-      },
-      template: `
-        <foo>
-          <template #[a]="one">a {{ one }} </template>
-          <template v-slot:[b]="two">b {{ two }} </template>
-        </foo>
-      `,
-      components: { Foo },
-    }).$mount()
-    expect(slotKeys).toEqual(['one', 'two'])
-    expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(
-      `a from foo one b from foo two`
-    )
-    vm.a = 'two'
-    vm.b = 'three'
-    waitForUpdate(() => {
-      // expect(slotKeys).toEqual(['one', 'three']);
-      expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(
-        `a from foo two b from foo three `
-      )
-    }).then(done)
-  })
-})
diff --git a/test/setupContext.spec.ts b/test/setupContext.spec.ts
new file mode 100644
index 0000000..b6ccae2
--- /dev/null
+++ b/test/setupContext.spec.ts
@@ -0,0 +1,199 @@
+import {
+  h,
+  defineComponent,
+  createApp,
+  ref,
+  nextTick,
+  SetupContext,
+} from '../src'
+
+describe('setupContext', () => {
+  it('should have proper properties', () => {
+    let context: SetupContext = undefined!
+
+    const vm = createApp(
+      defineComponent({
+        setup(_, ctx) {
+          context = ctx
+        },
+        template: '<div/>',
+      })
+    ).mount()
+
+    expect(context).toBeDefined()
+    expect('parent' in context).toBe(true)
+    expect(context.root).toBe(vm.$root)
+    expect(context.parent).toBe(vm.$parent)
+    expect(context.slots).toBeDefined()
+    expect(context.attrs).toEqual(vm.$attrs)
+    expect(context.listeners).toBe(vm.$listeners)
+
+    // CAUTION: this will be removed in 3.0
+    // @ts-expect-error
+    expect(context.refs).toBe(vm.$refs)
+    expect(typeof context.emit === 'function').toBe(true)
+  })
+
+  it('slots should work in render function', () => {
+    const vm = createApp(
+      defineComponent({
+        template: `
+        <test>
+          <template slot="default">
+            <span>foo</span>
+          </template>
+          <template slot="item" slot-scope="props">
+            <span>{{ props.text || 'meh' }}</span>
+          </template>
+        </test>
+      `,
+        components: {
+          test: defineComponent({
+            setup(_, { slots }) {
+              return () => {
+                return h('div', [slots.default(), slots.item()])
+              }
+            },
+          }),
+        },
+      })
+    ).mount()
+    expect(vm.$el.innerHTML).toBe('<span>foo</span><span>meh</span>')
+  })
+
+  it('warn for slots calls outside of the render() function', () => {
+    let warn = jest
+      .spyOn(global.console, 'error')
+      .mockImplementation(() => null)
+
+    createApp(
+      defineComponent({
+        template: `
+          <test>
+            <template slot="default">
+              <span>foo</span>
+            </template>
+          </test>
+        `,
+        components: {
+          test: {
+            setup(_, { slots }) {
+              slots.default()
+            },
+          },
+        },
+      })
+    ).mount()
+    expect(warn.mock.calls[0][0]).toMatch(
+      'slots.default() got called outside of the "render()" scope'
+    )
+    warn.mockRestore()
+  })
+
+  it('staled slots should be removed', () => {
+    const Child = {
+      template: '<div><slot value="foo"/></div>',
+    }
+    const vm = createApp(
+      defineComponent({
+        components: { Child },
+        template: `
+          <child>
+            <template slot-scope="{ value }" v-if="value">
+              foo {{ value }}
+            </template>
+          </child>
+        `,
+      })
+    ).mount()
+    expect(vm.$el.textContent).toMatch(`foo foo`)
+  })
+
+  it('slots should be synchronized', async () => {
+    let slotKeys: string[] = []
+
+    const Foo = defineComponent({
+      setup(_, { slots }) {
+        slotKeys = Object.keys(slots)
+        return () => {
+          slotKeys = Object.keys(slots)
+          return h('div', [
+            slots.default && slots.default('from foo default'),
+            slots.one && slots.one('from foo one'),
+            slots.two && slots.two('from foo two'),
+            slots.three && slots.three('from foo three'),
+          ])
+        }
+      },
+    })
+
+    const vm = createApp(
+      defineComponent({
+        data() {
+          return {
+            a: 'one',
+            b: 'two',
+          }
+        },
+        template: `
+          <foo>
+            <template #[a]="one">a {{ one }} </template>
+            <template v-slot:[b]="two">b {{ two }} </template>
+          </foo>
+        `,
+        components: { Foo },
+      })
+    ).mount()
+
+    expect(slotKeys).toEqual(['one', 'two'])
+    expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(
+      `a from foo one b from foo two`
+    )
+
+    // @ts-expect-error
+    vm.a = 'two'
+    // @ts-expect-error
+    vm.b = 'three'
+
+    await nextTick()
+    // expect(slotKeys).toEqual(['one', 'three']);
+    expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(
+      `a from foo two b from foo three `
+    )
+  })
+
+  // #264
+  it('attrs should be reactive after destructuring', async () => {
+    let _attrs: SetupContext['attrs'] = undefined!
+    const foo = ref('bar')
+
+    const ComponentA = defineComponent({
+      setup(_, { attrs }) {
+        _attrs = attrs
+      },
+      template: `<div>{{$attrs}}</div>`,
+    })
+    const Root = defineComponent({
+      components: {
+        ComponentA,
+      },
+      setup() {
+        return { foo }
+      },
+      template: `
+        <ComponentA :foo="foo"/>
+      `,
+    })
+
+    createApp(Root).mount()
+
+    expect(_attrs).toBeDefined()
+    expect(_attrs.foo).toBe('bar')
+
+    foo.value = 'bar2'
+
+    await nextTick()
+
+    expect(_attrs.foo).toBe('bar2')
+  })
+})