vue-async-computed-decorator
vue-async-computed-decorator copied to clipboard
incorrect return type
Hi, thanks for your work on this - really makes life easier. I've absolutely no idea about decorators or whether it's even possible, but atm I'm unable to use async computed properties outside of templates because at the moment typescript believes they're a method returning a promise. e.g:

The first computed property will, in-fact, work just fine if I force typescript to ignore it, and the second async property will obviously fail although ts is fine with it:

Having the exact same issue, it worked in vue-async-computed using the composition style, but not class based.
Anyway, here's a workaround for now, cast to unknown and then cast to what you know it is.:
const c = this.countries as unknown;
const countries = c as any[];
countries.forEach((a) => {
console.log(a);
});
But I would also like to know how to fix this 'properly' and also have no knowledge of decorators :)
Good question! I actually don't have a good answer. Perhaps @nwtgck (who contributed this decorator) knows?
As a bit of a workaround, I suppose you could wrap the async computed property in a normal getter / computed property with a type assertion, something like this:
@Component({})
class MyComponent {
@AsyncComputed()
async countriesAsync () {
/* your implementation here */
}
get countries() {
return this.countriesAsync as unknown as Country[]
}
get countryName () {
/* your implementation of countryName, using this.countries */
}
}
This takes @DGollings' workaround, and formalizes it a bit in a separate getter which passes through the value of the async computed property with a type assertion. This is an unfortunate amount of boilerplate: vue-async-computed exists in the first place to avoid having to have this sort of multiple-layer boilerplate to use asynchronously computed values, but typescript doesn't seem to be capable of realizing that, at least not when used with class-style components using a decorator.
Further improving on above suggestions, below is a generic unwrap method, to cast AsyncComputed properties to normal values:
@Component({})
export default class MyPage extends Vue {
//// Reusable function
// Casts an Async computed field to its synchronous return value
$asyncUnwrap<T>(promise: () => Promise<T>): T {
return (promise as unknown) as T
}
//// Example usage
// Given an async computed property
@AsyncComputed()
async users(): Promise<UserDto[] | undefined> {
return await fetch(`http://example.com/users` ).then(response => response.data.users)
// Re-using the property in another computed property
get userCount() {
return this.$asyncUnwrap(this.users)?.length
}
You could place this method in a component base class (and extend from it instead of Vue), or write a small plugin.
Plugin
Plugin to make this method available in all Vue components
async-unwrap.ts
function asyncUnwrap<T>(promise: () => Promise<T>): T {
return (promise as unknown) as T
}
declare module 'vue/types/vue' {
interface Vue {
$asyncUnwrap: typeof asyncUnwrap
}
}
export const AsyncUnwrapModule = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-explicit-any
install: (Vue: any, options: any) => {
Vue.prototype.$asyncUnwrap = asyncUnwrap
},
}
main.ts
// install
Vue.use(AsyncUnwrapModule);
usage:
// Inside a component function
this.$asyncUnwrap(this.someAsyncComputedProperty);
After re-consideration, I realize the plugin approach is overkill, since the method can be static.
I suggest the following instead.
Global $asyncUnwrap method
/util/async-unwrap.ts
// noinspection JSUnusedGlobalSymbols
export default function $asyncUnwrap<T>(promise: () => Promise<T>): T {
return (promise as unknown) as T
}
usage:
import $asyncUnwrap from '@/util/async-unwrap'
@Component
export default class MyPage extends Vue {
@AsyncComputed()
async users(): Promise<User[] | undefined> {
return fetch(/* ... */);
}
get userCount(): number | undefined {
return $asyncUnwrap(this.users)?.length;
}
}
Using @boukeversteegh unwrap function, i created this plugin which provides a method on the Vue instance.
/// asyncUnwrap.ts
import _Vue from "vue";
/**
* Type declaration for the new Vue attribute
*/
declare module 'vue/types/vue' {
interface Vue {
$asyncUnwrap<T>(promise: () => Promise<T>): T
}
}
/**
* Unwraps the type of the promise used in @AsyncComputed decorator & computed property annotations.
* @param Vue
* @param options
*/
export default function AsyncUnwrapPlugin(Vue: typeof _Vue, options?: any): void {
Vue.prototype.$asyncUnwrap = function<T>(promise: () => Promise<T>): T {
return (promise as unknown) as T
};
}
Then in main.ts:
import AsyncUnwrapPlugin from "./plugins/asyncUnwrap"
Vue.use(AsyncUnwrapPlugin)
Using @boukeversteegh unwrap function, i created this plugin which provides a method on the Vue instance.
Hm yeah, it's what I went with at first as well, but I didn't like to write this. so changed it to a global function instead :-)
In general, after working with vue-async-computed for the past 8 months (I have 44 async properties), I would recommend to combine the $asyncUnwrap with the approach suggested by @foxbenjaminfox:
Define two properties:
- an
@AsyncComputedproperty suffixed withAsync() - a normal computed property that unwraps the first using
$asyncUnwrap
Motivation:
- More easy to refactor: no need to remove or add
$asyncUnwrapeverywhere when refactoring a property from or to async-computed. - More encapsulated: other parts of your code won't have to know whether the property was async-computed. You only use the normal properties.
- Applying the
Asyncsuffix by itself will ensure that it's clear everywhere when you do and don't need to unwrap.
Component()
class MyVueComponent extends Vue {
@AsyncComputed()
async usersAsync(): Promise<User[]> {
return await // ...
}
get users(): User[] {
return $asyncUnwrap(this.usersAsync);
}
}
Hey @dayre ! I just made a PR that will hopefully make it much easier to get the return types correctly!
Have a look #22