Skip to content

incorrect return type #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
johnjenkins opened this issue Jul 17, 2020 · 7 comments
Open

incorrect return type #13

johnjenkins opened this issue Jul 17, 2020 · 7 comments

Comments

@johnjenkins
Copy link

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

@DGollings
Copy link

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

@foxbenjaminfox
Copy link
Owner

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.

@boukeversteegh
Copy link

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
Copy link

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

@dayre
Copy link

dayre commented Feb 1, 2022

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)

@boukeversteegh
Copy link

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
Copy link

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

Have a look #22

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants