Skip to content

Async computed getter #22

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
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

boukeversteegh
Copy link

@boukeversteegh boukeversteegh commented Feb 2, 2022

This PR adds support to use @AsyncComputed() on getter methods:

  @AsyncComputed()
  get user(): User {
    return fetch('/api/users/me')
       .then(r => r.json())
       .then(data => data.user) as any
  }

This allows for greatly improved type-checking and IDE support for AsyncComputed.

Benefits:

  • You can force the return type to be User instead of Promise<User>, which is what the eventual type will be.
    • This trick is only possible with getters, because all async functions must return a Promise.
  • Editors will understand this.user.name and will not expect this.user().name as is the case now.
  • Otherwise, the plugin works exactly the same as with async functions. All options supported.
  • We can use async-computed in a mostly type-safe manner without much boilerplate.

Drawbacks:

  • await is not allowed in getters, so you must use only .then and .catch
  • the intermediate cast to any is not type-safe, and may hide incorrect casts.

To work around these limitations, I suggest the following helper method. It is not included, but it could be if approved.

// Cast a Promise<T> to T.
// Accepts a promise or async function.
function asyncComputed<T>(promise: Promise<T> | (() => Promise<T>)): T {
  return (typeof promise === 'function' ? promise() : promise) as any
}

This can be used as follows:

  @AsyncComputed()
  get users(): User {
    return asyncComputed(fetchUserAsync()) // safely cast Promise<User> to User
  }

  @AsyncComputed()
  get users(): User {
    return asyncComputed(async () => {
      return await fetchUserAsync() // using await within the getter
    })
  }

@boukeversteegh boukeversteegh marked this pull request as draft February 2, 2022 20:36
@boukeversteegh
Copy link
Author

boukeversteegh commented Feb 2, 2022

On hold while applying this to my own codebase

@dayre
Copy link

dayre commented Feb 2, 2022

Thank you for putting so much thought into this @boukeversteegh. At first glance, the only thing that i find potentially confusing is the naming of the asyncComputed() helper method.

It has the same name as the @AsyncComputed decorator as well as the $asyncComputed object containing the async state and doesn't really describe what it is doing. It really is unwrapping the promise, so perhaps rename to asyncUnwrap() ?

@boukeversteegh
Copy link
Author

I've added a prepare script to allow installing the package from github.

"scripts": {
"check": "npm run build && npm run lint -s && npm run test -s",
"build": "tsc",
"lint": "tslint -c tslint.json 'src/**/*.ts'",
"prepare": "npm run build",
"prepublishOnly": "npm run -s check",
"test": "npm run build && parcel build test/index.js --out-dir test/dist && node test/dist/index.js | tspec"
},

This is needed because otherwise npm install script will clean out all src/ files, and not build anything, so you end up with an empty package. Normally that is OK because you install from the published npm package, and the build/ will be kept, but since we don't check in build/, the package must be built locally when installing from source.

  • It's a bit strange, but there doesn't seem to be a way to use npm packages from source, and actually use the source.

You can try out this fork as follows:

npm install datastack-net/vue-async-computed-decorator#feature/async-computed-getter

Installing from github is slow, it may take a few minutes!

@boukeversteegh
Copy link
Author

Thank you for putting so much thought into this @boukeversteegh. At first glance, the only thing that i find potentially confusing is the naming of the asyncComputed() helper method.

It has the same name as the @AsyncComputed decorator as well as the $asyncComputed object containing the async state and doesn't really describe what it is doing. It really is unwrapping the promise, so perhaps rename to asyncUnwrap() ?

You're welcome. It was a long-standing issue for my own project as well, so I felt like finally getting to the bottom of it.

I understand your point that the name is not very clear. It's behavior is also a bit unusual, so it's hard to find a good name for it.

It has a dual purpose:

  • Forcefully casting Promises to their inner type. It unwraps the type, but not the values. Just a hack to trick typescript and improve IDE suggestions.
  • Helper to use await within an IIFE

In some way, it is unwrapping, but only the type, and 'unwrap' suggests it does more than just casting.

Perhaps asyncUnwrap is still the better name. For now, the function is not included, so users can just define their own function. Some other potentially bad names could: unpromise(), async, unwrapPromise, synchronize, asSynchronous, asSync, castAsPromiseInnerType, extractType, recastAsync. I don't know, all of them seem weird.

I've also been experimenting with an alternative decorator that just synchronizes a async fooAsync(): Promise<T> to foo: T. This requires one extra line to define foo: T, but all the types will be logical, and no magic is involved.

In any case, this PR is ready for testing. I'm still testing it myself within a big refactor, so it may take a while.

@boukeversteegh boukeversteegh marked this pull request as ready for review February 4, 2022 13:13
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

Successfully merging this pull request may close these issues.

2 participants