Repository: vuejs/composition-api
Pull request #820 (2021-10-01 09:46:48)

feat(computed): allow differentiating refs from computed
Add an `effect` property on computed to differentiate them from refs

---

Another possible implementation is to keep the computed types where they were and add an option effect to ref

posva: I don't understand why the node v12 is failing 😓 

Patch:
diff --git a/README.md b/README.md
index 6528db2..1f617ec 100644
--- a/README.md
+++ b/README.md
@@ -445,6 +445,24 @@ defineComponent({
 
 </details>
 
+### `computed().effect`
+
+<details>
+<summary>
+⚠️ <code>computed()</code> has a property <code>effect</code> set to <code>true</code> instead of a <code>ReactiveEffect<T></code>.
+</summary>
+
+Due to the difference in implementation, there is no such concept as a `ReactiveEffect` in `@vue/composition-api`. Therefore, `effect` is merely `true` to enable differentiating computed from refs:
+
+```ts
+function isComputed<T>(o: ComputedRef<T> | unknown): o is ComputedRef<T>
+function isComputed(o: any): o is ComputedRef {
+  return !!(isRef(o) && o.effect)
+}
+```
+
+</details>
+
 ### Missing APIs
 
 The following APIs introduced in Vue 3 are not available in this plugin.
diff --git a/src/apis/computed.ts b/src/apis/computed.ts
index 2cac40a..fc85a5e 100644
--- a/src/apis/computed.ts
+++ b/src/apis/computed.ts
@@ -1,5 +1,5 @@
 import { getVueConstructor } from '../runtimeContext'
-import { createRef, Ref } from '../reactivity'
+import { createRef, ComputedRef, WritableComputedRef } from '../reactivity'
 import {
   warn,
   noopFn,
@@ -9,12 +9,6 @@ import {
 } from '../utils'
 import { getCurrentScopeVM } from './effectScope'
 
-export interface ComputedRef<T = any> extends WritableComputedRef<T> {
-  readonly value: T
-}
-
-export interface WritableComputedRef<T> extends Ref<T> {}
-
 export type ComputedGetter<T> = (ctx?: any) => T
 export type ComputedSetter<T> = (v: T) => void
 
@@ -102,6 +96,7 @@ export function computed<T>(
       get: computedGetter,
       set: computedSetter,
     },
-    !setter
-  )
+    !setter,
+    true
+  ) as WritableComputedRef<T> | ComputedRef<T>
 }
diff --git a/src/apis/watch.ts b/src/apis/watch.ts
index 91f68c3..633cdd1 100644
--- a/src/apis/watch.ts
+++ b/src/apis/watch.ts
@@ -1,5 +1,5 @@
 import { ComponentInstance } from '../component'
-import { Ref, isRef, isReactive } from '../reactivity'
+import { Ref, isRef, isReactive, ComputedRef } from '../reactivity'
 import {
   assert,
   logError,
@@ -18,7 +18,6 @@ import {
   WatcherPreFlushQueueKey,
   WatcherPostFlushQueueKey,
 } from '../utils/symbols'
-import { ComputedRef } from './computed'
 import { getCurrentScopeVM } from './effectScope'
 
 export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void
diff --git a/src/component/componentProps.ts b/src/component/componentProps.ts
index bcd7e73..d522f2f 100644
--- a/src/component/componentProps.ts
+++ b/src/component/componentProps.ts
@@ -63,7 +63,9 @@ type InferPropType<T> = T extends null
                 ? Function
                 : T extends Prop<infer V, infer D>
                   ? unknown extends V
-                    ? D
+                    ? D extends null | undefined
+                      ? V
+                      : D
                     : ExtractCorrectPropType<V>
                   : T
 
diff --git a/src/reactivity/index.ts b/src/reactivity/index.ts
index 71f53de..c9b2ea1 100644
--- a/src/reactivity/index.ts
+++ b/src/reactivity/index.ts
@@ -24,6 +24,8 @@ export { del } from './del'
 
 export type {
   Ref,
+  ComputedRef,
+  WritableComputedRef,
   ToRefs,
   UnwrapRef,
   UnwrapRefSimple,
diff --git a/src/reactivity/ref.ts b/src/reactivity/ref.ts
index bf49d1c..210d7f8 100644
--- a/src/reactivity/ref.ts
+++ b/src/reactivity/ref.ts
@@ -9,6 +9,19 @@ export interface Ref<T = any> {
   value: T
 }
 
+export interface WritableComputedRef<T> extends Ref<T> {
+  /**
+   * `effect` is added to be able to differentiate refs from computed properties.
+   * **Differently from Vue 3, it's just `true`**. This is because there is no equivalent
+   * of `ReactiveEffect<T>` in `@vue/composition-api`.
+   */
+  effect: true
+}
+
+export interface ComputedRef<T = any> extends WritableComputedRef<T> {
+  readonly value: T
+}
+
 export type ToRefs<T = any> = { [K in keyof T]: Ref<T[K]> }
 
 export type CollectionTypes = IterableCollections | WeakCollections
@@ -58,14 +71,22 @@ export class RefImpl<T> implements Ref<T> {
   }
 }
 
-export function createRef<T>(options: RefOption<T>, readonly = false) {
+export function createRef<T>(
+  options: RefOption<T>,
+  isReadonly = false,
+  isComputed = false
+): RefImpl<T> {
   const r = new RefImpl<T>(options)
+
+  // add effect to differentiate refs from computed
+  if (isComputed) (r as ComputedRef<T>).effect = true
+
   // seal the ref, this could prevent ref from being observed
   // It's safe to seal the ref, since we really shouldn't extend it.
   // related issues: #79
   const sealed = Object.seal(r)
 
-  if (readonly) readonlySet.set(sealed, true)
+  if (isReadonly) readonlySet.set(sealed, true)
 
   return sealed
 }


Test patch:
diff --git a/test-dts/defineComponent.test-d.ts b/test-dts/defineComponent.test-d.ts
index 8856d83..f1305df 100644
--- a/test-dts/defineComponent.test-d.ts
+++ b/test-dts/defineComponent.test-d.ts
@@ -28,6 +28,7 @@ describe('with object props', () => {
     ffff: (a: number, b: string) => { a: boolean }
     validated?: string
     date: Date
+    unknown: unknown
   }
 
   type GT = string & { __brand: unknown }
@@ -100,6 +101,10 @@ describe('with object props', () => {
         type: Date,
         required: true,
       },
+      unknown: {
+        type: Object as PropType<unknown>,
+        default: null,
+      },
     },
     setup(props) {
       // type assertion. See https://github.com/SamVerschueren/tsd
@@ -121,6 +126,7 @@ describe('with object props', () => {
       expectType<ExpectedProps['ffff']>(props.ffff)
       expectType<ExpectedProps['validated']>(props.validated)
       expectType<ExpectedProps['date']>(props.date)
+      expectType<typeof props.unknown>({} as ExpectedProps['unknown'])
 
       isNotAnyOrUndefined(props.a)
       isNotAnyOrUndefined(props.b)
@@ -171,6 +177,7 @@ describe('with object props', () => {
       expectType<ExpectedProps['hhh']>(props.hhh)
       expectType<ExpectedProps['ffff']>(props.ffff)
       expectType<ExpectedProps['validated']>(props.validated)
+      expectType<typeof props.unknown>({} as ExpectedProps['unknown'])
 
       // @ts-expect-error props should be readonly
       expectError((props.a = 1))
diff --git a/test/apis/computed.spec.js b/test/apis/computed.spec.js
index 78606e4..af0ce83 100644
--- a/test/apis/computed.spec.js
+++ b/test/apis/computed.spec.js
@@ -1,5 +1,5 @@
 const Vue = require('vue/dist/vue.common.js')
-const { ref, computed, isReadonly } = require('../../src')
+const { ref, computed, isReadonly, reactive, isRef } = require('../../src')
 
 describe('Hooks computed', () => {
   beforeEach(() => {
@@ -212,4 +212,30 @@ describe('Hooks computed', () => {
     expect(isReadonly(z.value)).toBe(false)
     expect(isReadonly(z.value.a)).toBe(false)
   })
+
+  it('passes isComputed', () => {
+    function isComputed(o) {
+      return !!(o && isRef(o) && o.effect)
+    }
+
+    expect(isComputed(computed(() => 2))).toBe(true)
+    expect(
+      isComputed(
+        computed({
+          get: () => 2,
+          set: () => {},
+        })
+      )
+    ).toBe(true)
+
+    expect(isComputed(ref({}))).toBe(false)
+    expect(isComputed(reactive({}))).toBe(false)
+    expect(isComputed({})).toBe(false)
+    expect(isComputed(undefined)).toBe(false)
+    expect(isComputed(null)).toBe(false)
+    expect(isComputed(true)).toBe(false)
+    expect(isComputed(20)).toBe(false)
+    expect(isComputed('hey')).toBe(false)
+    expect(isComputed('')).toBe(false)
+  })
 })