Subscribe on changes!

problem with `reactive` handling objects with inner properties that are classes

avatar
May 14th 2022

Link to minimal reproduction

https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCAgbGFuZz1cInRzXCIgc2V0dXA+XG5cbiAgaW1wb3J0IHsgcmVhY3RpdmUgfSBmcm9tIFwidnVlXCI7XG5cbiAgY2xhc3MgVGVzdEdlbmVyaWM8ZVggZXh0ZW5kcyBudW1iZXI+IHtcbiAgICBwcm90ZWN0ZWQgeCE6IG51bWJlcjtcbiAgICBjb25zdHJ1Y3Rvcihwcm90ZWN0ZWQgeTogZVgpIHtcbiAgICAgIHRoaXMueCA9IHk7XG4gICAgfVxuICB9XG5cbiAgY2xhc3MgVGVzdENsYXNzIHtcbiAgICBwcm90ZWN0ZWQgeCE6IG51bWJlcjtcbiAgICBjb25zdHJ1Y3Rvcihwcm90ZWN0ZWQgeTogbnVtYmVyKSB7XG4gICAgICB0aGlzLnggPSB5O1xuICAgIH1cbiAgfVxuXG4gIGludGVyZmFjZSBJVGVzdCB7XG4gICAgeDogbnVtYmVyO1xuICAgIHk6IHN0cmluZyB8IHVuZGVmaW5lZDtcbiAgICB0ZXN0R2VuZXJpYzogVGVzdEdlbmVyaWM8bnVtYmVyPiB8IHVuZGVmaW5lZDtcbiAgICB0ZXN0Q2xhc3M6IFRlc3RDbGFzcyB8IHVuZGVmaW5lZDtcbiAgfVxuXG4gIGNvbnN0IHRlc3RPYmo6IElUZXN0ID0ge1xuICAgIHg6IDMsXG4gICAgeTogdW5kZWZpbmVkLFxuICAgIHRlc3RHZW5lcmljOiB1bmRlZmluZWQsXG4gICAgdGVzdENsYXNzOiB1bmRlZmluZWQsXG4gIH07XG5cbiAgLyoqIHRoZXJlIGlzIGVycm9yIG9uIHRoZSBgdGVzdGAgY29uc3RhbnQsIGFzIHJlYWN0aXZlPElUZXN0PiBkb2Vzbid0IHByb3ZpZGUgYElUZXN0YCB0eXBlIHByb3Blcmx5LiBjbGFzc2VzIGBUZXN0R2VuZXJpY2AgYW5kIGBUZXN0Q2xhc3NgIGFyZSBcInRyaW1tZWRcIiB0byBwdXJlIGB7fWAgdHlwZXMgYW5kIHdlIGdhdCBhbiBlcnJvciBsaWtlIFwiYHt9YCBkb2Vzbid0IGNvbnRhaW4gYHhgIGFuZCBgeWBcIi5cbiAgICogSWYgSSByZW1vdmUgYHRlc3RHZW5lcmljYCBhbmQgYHRlc3RDbGFzc2AgbmF0dXJhbGx5LCBlcnJvcnMgYXJlIGdvbmUuXG4gICAqL1xuICBjb25zdCB0ZXN0OiBJVGVzdCA9IHJlYWN0aXZlPElUZXN0Pih0ZXN0T2JqKTtcbiAgY29uc29sZS5sb2coYHRlc3Q6ICR7dGVzdH1gKTtcblxuPC9zY3JpcHQ+XG5cbjx0ZW1wbGF0ZT5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly91bnBrZy5jb20vQHZ1ZS9ydW50aW1lLWRvbUAzLjIuMzMvZGlzdC9ydW50aW1lLWRvbS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0ifQ==

Steps to reproduce

It is self-contained in the provided the SFC Playground reproduction

What is expected?

It is expected that

const test: ITest = reactive<ITest>(testObj);

works properly in regard to type matching.

What is actually happening?

I get an error from the TypeScript language server in VSC.

TypeScript types matching with reactive are not handling properly classes. It reduces them to empty objects {}

System Info

System:
    OS: macOS 11.4
    CPU: (8) x64 Apple M1
    Memory: 21.72 MB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 16.13.0 - /usr/local/bin/node
    Yarn: 1.22.5 - ~/.yarn/bin/yarn
    npm: 8.1.0 - /usr/local/bin/npm
  Browsers:
    Chrome: 101.0.4951.64
    Chrome Canary: 104.0.5061.0
    Edge: 96.0.1054.62
    Firefox: 99.0
    Safari: 14.1.1
  npmPackages:
    vue: ^3.2.33 => 3.2.33

Any additional comments?

If this is a technical limitation and we have to do some workarounds it should be mentioned in reactive doc but there is no mention.

I can imagine we should use interfaces instead of classes but again, it would be a serious drawback for various use-cases.

avatar
May 14th 2022

I have noticed that the SFC Playground doesn't handle the code properly so here it is again:

<script  lang="ts" setup>

  import { reactive } from "vue";

  class TestGeneric<eX extends number> {
    protected x!: number;
    constructor(protected y: eX) {
      this.x = y;
    }
  }

  class TestClass {
    protected x!: number;
    constructor(protected y: number) {
      this.x = y;
    }
  }

  interface ITest {
    x: number;
    y: string | undefined;
    testGeneric: TestGeneric<number> | undefined;
    testClass: TestClass | undefined;
  }

  const testObj: ITest = {
    x: 3,
    y: undefined,
    testGeneric: undefined,
    testClass: undefined,
  };

  /** there is error on the `test` constant, as reactive<ITest> doesn't provide `ITest` type properly. classes `TestGeneric` and `TestClass` are "trimmed" to pure `{}` types and we gat an error like "`{}` doesn't contain `x` and `y`".
   * If I remove `testGeneric` and `testClass` naturally, errors are gone.
   */
  const test: ITest = reactive<ITest>(testObj);
  console.log(`test: ${test}`);

</script>

<template>
</template>
avatar
Aug 9th 2023

What's the status?

avatar
Oct 13th 2023

I ran into the same issue today, the issue has to do with protected properties on classes used inside reactive:

<script setup lang="ts">
import { reactive } from 'vue'

class Person {
  constructor(public name: string){}
  protected species = "HUMAN"
}

const names = [ "Jim", "Pam" ]
const people = reactive(names.map(name => new Person(name)))

function foo(people: Person[]){
  for (const person of people) {
    console.log(person instanceof Person) // all true
  }
}
foo(people)
//  ^ { name: string; }[] !== Person[]

</script>

<template>
  <p v-for="person in people">{{person}} - {{person instanceof Person}}</p>
                    <!-- ^ both objects are instances of Person class ^ -->
</template>

Playground link.

@LinusBorg this was never triaged/picked up by the Vue team, it seems.

avatar
Oct 13th 2023

I can add: I think this is because UnwrapRefSimple<T> depends on the keyof operator, which according to this comment in the TS repo "intentionally does not include non-public members".

So it might not be something that we can get around, unless the type can be made without the usage of the keyof operator, or something like fieldof is added to TypeScript.

Edit: I see now that there are other issues pointing this out.