vue-async-computed-decorator icon indicating copy to clipboard operation
vue-async-computed-decorator copied to clipboard

incorrect return type

Open johnjenkins opened this issue 5 years ago • 7 comments

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:

image

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:

image

johnjenkins avatar Jul 17 '20 21:07 johnjenkins

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 :)

DGollings avatar Jul 18 '20 00:07 DGollings

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.

foxbenjaminfox avatar Aug 14 '20 12:08 foxbenjaminfox

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);

boukeversteegh avatar Jun 17 '21 10:06 boukeversteegh

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;
   }
}

boukeversteegh avatar Jun 17 '21 12:06 boukeversteegh

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)

dayre avatar Feb 01 '22 22:02 dayre

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 @AsyncComputed property suffixed with Async()
  • a normal computed property that unwraps the first using $asyncUnwrap

Motivation:

  • More easy to refactor: no need to remove or add $asyncUnwrap everywhere 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 Async suffix 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);
  }

}

boukeversteegh avatar Feb 02 '22 10:02 boukeversteegh

Hey @dayre ! I just made a PR that will hopefully make it much easier to get the return types correctly!

Have a look #22

boukeversteegh avatar Feb 02 '22 17:02 boukeversteegh