I have a class using ES6 private fields and public getters that I need to be reactive using Vue 3's position API. Currently, my set up looks something like this:
//store.ts
class Store {
#userName?: string
get userName() {
if(this.#userName !== undefined) return this.#userName
throw new Error('Cannot get userName before it is defined')
}
setUserName(newUserName: string) {
this.#userName = newUserName
}
}
const store = reactive(new Store())
export { store }
This store instance is then provided to ponents through the provide/inject API, and used like so
<template>
<span> {{ formattedUserName }} </span>
</template>
<script lang="ts">
import { defineComponent, toRefs, inject } from 'vue'
export default defineComponent({
setup(){
const store = inject('storeKey')
const formattedUserName = doSomeThings(store.userName)
return { formattedUserName }
})
But when I try to do this I get this error:
Cannot read private member #userName from an object whose class did not declare it
Could someone explain why this is, and if there's a way around it? I know TypeScript has the private
keyword, but if possible I'd like to use private fields since they actually enforce the privacy at runtime.
Thanks!
Edit: After a bit more research, I've found the answer to the "why" part of my question. Vue 3 uses Proxy to track reactivity, which unfortunately does not work with private fields. If there is any way around this I would still love to know.
I have a class using ES6 private fields and public getters that I need to be reactive using Vue 3's position API. Currently, my set up looks something like this:
//store.ts
class Store {
#userName?: string
get userName() {
if(this.#userName !== undefined) return this.#userName
throw new Error('Cannot get userName before it is defined')
}
setUserName(newUserName: string) {
this.#userName = newUserName
}
}
const store = reactive(new Store())
export { store }
This store instance is then provided to ponents through the provide/inject API, and used like so
<template>
<span> {{ formattedUserName }} </span>
</template>
<script lang="ts">
import { defineComponent, toRefs, inject } from 'vue'
export default defineComponent({
setup(){
const store = inject('storeKey')
const formattedUserName = doSomeThings(store.userName)
return { formattedUserName }
})
But when I try to do this I get this error:
Cannot read private member #userName from an object whose class did not declare it
Could someone explain why this is, and if there's a way around it? I know TypeScript has the private
keyword, but if possible I'd like to use private fields since they actually enforce the privacy at runtime.
Thanks!
Edit: After a bit more research, I've found the answer to the "why" part of my question. Vue 3 uses Proxy to track reactivity, which unfortunately does not work with private fields. If there is any way around this I would still love to know.
Share Improve this question edited Jul 17, 2023 at 20:33 Connor Dooley asked Aug 9, 2021 at 17:03 Connor DooleyConnor Dooley 3892 silver badges13 bronze badges 4- 2 they actually enforce the privacy at runtime - they really do. To the point that they can't be accessed by anybody but this class, no matter how, even you or the framework that you use needs that (more specifically, Proxy, as you said). I'd advise against using things that can bee a footgun any time, at least without a very good reason. Native private field is certainly one of them. – Estus Flask Commented Aug 9, 2021 at 18:24
- 1 If there is any way around this I would still love to know. - probably by forcing Babel transform for private fields if it doesn't cause this error. Notice that it relies on specific Babel implementation, the fix is fragile and can break any moment. – Estus Flask Commented Aug 9, 2021 at 18:25
- 1 I don't think this Q should be tagged typescript. Your problem is with ES6 private fields, not Typescript private fields. There is a difference. – spinkus Commented Jul 13, 2023 at 5:04
- @spinkus good point, I'll see if I can remove that – Connor Dooley Commented Jul 17, 2023 at 20:32
2 Answers
Reset to default 11So short answer: There is no simple way to still use private fields and Proxy together. Private fields and Proxy, as they are implemented now (August 10th, 2021), are fundamentally inpatible. The second link in my original post has a ton of discussion on why that is and whether it should be changed, but without a new proposal it looks like this is just the way things are.
Here's how I got a store together that has
- pile time privacy for its members
- reactive, readonly (also at pile time) getters for those members that guarantee the returned value is not undefined
- specific setters that can also have side effects
import {ref, reactive, puted} from 'vue'
class Store {
private internal_userName: Ref<string | undefined> = ref(undefined)
readonly userName = puted((): string => {
if(this.internal_userName.value !== undefined) return this.internal_userName.value
throw new Error('Cannot access userName before it is defined')
})
setUserName(newUserName: string) {
// do side effects here, such as setting localStorage keys
this.internal_userName.value = newUserName
}
}
const store = reactive(new Store())
//do things to export or provide your store here
As accepted answer is correct - ES6 private fields don't work with Vue reactive object wrappers. But if your already using typescript source you can use Typescript private
declarations instead of ES6 private fields. The former are deleted at pilation to Javascript (ES6 or not), the latter survive and are meaningful at runtime. There is still caveats as mentioned in #2981 to do with the type returned by reactive
or ref
not actually being related to the class of object you passed in, but pretty sure you can safely type assert like const myReactiveFoo: Foo = reactive<Foo>(someFooInstance) as Foo
or just not bother and accept type returned from reactive
.