I have a class used in my VueJS project. I have a function on it which returns a calculated value. If I make this a computed property then it works fine in my VueJS application.
However, Typescript shows an error when I try to push an instance of this class to an array due to the computed property. I can resolve either problem but not both so what is the correct way to do this?
Argument of type 'Person' is not assignable to parameter of type '{ firstname: string; lastname: string; fullName: string; }'. Types of property 'fullName' are incompatible. Type 'ComputedRef' is not assignable to type 'string'.
The class, store and app code:
//person.ts
import { computed } from 'vue'
export default class Person {
firstname: string
lastname: string
constructor(firstname: string, lastname: string) {
this.firstname = firstname
this.lastname = lastname
}
//This works in my app but throws warnings on the array push
fullName = computed(() => {
return this.lastname + ', ' + this.firstname
})
//This is fine on the array push but doesn't work in my app.
// fullName() {
// return this.lastname + ', ' + this.firstname
// }
}
//personStore.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import Person from '@/modules/person'
export const usePersonStore = defineStore('personStore', () => {
const persons = ref<Array<Person>>([])
function add(person: Person) {
//TypeScript PROBLEM IS THE LINE BELOW
persons.value.push(person)
}
return { persons, add }
})
//App.vue
<script setup lang="ts">
import Person from './modules/person';
import { usePersonStore } from './stores/personStore';
const personStore = usePersonStore()
let bob = new Person('Robert', 'McRobert')
personStore.add(bob)
</script>
<template>
<h1 v-for="person in personStore.persons">{{ person.fullName }}</h1>
</template>
If I use the computed version of the function in the class, my app will correctly show "McRobterts, Robert" on the screen. If use the function version then, unsurprisingly, it doesn't call the function it just shows fullName() { return this.lastname + ", " + this.firstname; }
I know it needs to be a computed property, I included the non-computed version to show that I can push an object to the array when it's just a regular function. Do I have to move computed off the class and wrap the function around a computed property in the app? I could but I want to use this class in multiple components. Do I need to add some typing to TypeScript to tell it it's a computed class? The return type of the computed property is a string so not exactly sure what's right here.
I can of course make this work by just having a computed property in the app and if that's the right way to do it I can, but feels like it's more a lack of TypeScript knowledge on my part. This is obviously a simplified version.
In my full app I'll have a bunch of computed properties more complex and I'd like to keep them on the class than re-implement them in multiple components.
In this case this
is bound to non-reactive class instance in an arrow function in class field, this prevents a computed from being reactive. A workaround would be class getter, as suggested in the comments, they are defined on class prototype and can access correct reactive this
:
get fullName() { this.lastname + ', ' + this.firstname; }
It's not functionally identical to a computed, at least because computed results are cached, but this is an acceptable replacement for a simple computation.
Vue reactivity has are multiple pitfalls associated with classes, e.g. this and this. The use of refs (ref
, computed
, etc) as class fields will make a class work in an unexpected way both at compile and runtime because of ref unwrapping (person.fullName.value
becomes person.fullName
) and requires to handle this with additional measures.
This requires to be aware of all the pitfalls design a class with this specific usage in mind, like another answer suggests.
Vue reactivity was designed primarily for FP, a more straightforward way to write reusable code would be:
const createPerson = (name, lastname) => {
const person = reactive({
name,
lastname,
fullName: computed(() => person.lastname + ', ' + person.firstname));
});
return person;
};