Vue + TypeScriptで非同期な算出プロパティを使いたい！ - Promiseとかasync-await

## やりたいこと

**VueをTypeScript**でを使うときに、**非同期な（Promiseとかasync-await）オブジェクトを算出プロパティとして使いたい**です。

## 最終的な見た目

以下のように、`@AsyncComputed `をつけたasync functionが使えるようになります。

```ts
@Component
export default class HelloWorld extends Vue {

  @AsyncComputed()
  async myAsyncMessage(): Promise<string> {
    return new Promise(resolve => {
      setTimeout(()=>{
        resolve("This is an asynchronous message!");
      }, 1000);
    });
  }
}
```

テンプレートは以下みたいになります。

```vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    {{ myAsyncMessage }}
  </div>
</template>
```

## 動くコード

以下が、オンラインのデモページです。
開いて1秒後に「This is a asynchronous message!」と出るはずです。見逃したらリロードしてみてください。
<https://nwtgck-typescript-vue-async-computed-prac.netlify.com/>

![nwtgck-typescript-vue-async-computed-prac.mp4.opt.gif](https://qiita-image-store.s3.amazonaws.com/0/84451/30a5200c-0493-d18f-0ac5-dd334d12be57.gif)


## GitHubリポジトリ

こちらがリポジトリです。
なるべくミニマルに作るように心がけてます。
<https://github.com/nwtgck/typescript-vue-async-computed-prac>

以下の**gitの差分をみることで、どうやって`@AsyncComputed`に対応するかは、記事を読まなくても分かる**と思います。
<https://github.com/nwtgck/typescript-vue-async-computed-prac/commit/ed88abd38452722ff9da346d6f840a965c54812c>

「記事通りに進めば対応できる」を目指して、ステップバイステップで対応方法を書きたいと思います。


## 対応手順

[vue-async-computed](https://github.com/foxbenjaminfox/vue-async-computed)というライブラリを使います。
ただ、vue-async-computed公式がTypeScriptに対応していません。TypeScript対応に関してのissueは出ています。
Issue: <https://github.com/foxbenjaminfox/vue-async-computed/issues/25>
この対応手順はこのissueを読み解いたものになります。

---
追記（2019年12月24日）
つい４時間ほど前に3.8.0がリリースされて、公式にTypeScript対応されるようになりました！動作を確認して、その結果をこの記事にも反映できたらと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/11823949-6ec7-f38a-27c5-ea22f02d5cd4.png)
(<https://github.com/foxbenjaminfox/vue-async-computed/issues/25#issuecomment-568567343>)

---

まずは、以下のコマンドでライブラリをインストールします。

```bash
npm i -S vue-async-computed
```

次にvue-async-computedの型定義を追加します。
`types`というディレクトリを作ってその中に置きました。

```ts:types/vue-async-computed.d.ts
// (base: https://raw.githubusercontent.com/saraedum/vue-async-computed/5debb7dcd81f52183be55e05b866fc43278a9905/types/index.d.ts)

declare module 'vue-async-computed' {
  import Vue, { PluginFunction } from "vue";

  interface IAsyncComputedOptions {
    errorHandler?: (error: string[]) => void;
    useRawError?: boolean;
    default?: any;
  }
  
  export default class AsyncComputed {
    constructor(options?: IAsyncComputedOptions)
    static install: PluginFunction<never>;
    static version: string;
  }
  
  type AsyncComputedGetter<T> = () => (Promise<T> | T);
  export interface IAsyncComputedProperty<T> {
    default?: T | (() => T);
    get?: AsyncComputedGetter<T>;
    watch?: () => void;
    shouldUpdate?: () => boolean;
    lazy?: boolean;
  }
  
  interface IAsyncComputedProperties {
    [K: string]: AsyncComputedGetter<any> | IAsyncComputedProperty<any>;
  }
}

declare module "vue/types/options" {
  import Vue from "vue";
  import { IAsyncComputedProperties } from "vue-async-computed";

  export class InjectKey{}

  // tslint:disable-next-line:interface-name
  interface ComponentOptions<V extends Vue> {
    asyncComputed?: IAsyncComputedProperties;
  }
}

interface IASyncComputedState {
  state: "updating" | "success" | "error";
  updating: boolean;
  success: boolean;
  error: boolean;
  exception: Error | null;
  update: () => void;
}

declare module "vue/types/vue" {
  // tslint:disable-next-line:interface-name
  interface Vue {
    $asyncComputed: {[K: string]: IASyncComputedState };
  }
}
```

以下のように、`tsconfig.json`に`"*": [ "types/*"]`を追加します。
これは上記の`vue-async-computed.d.ts`を違う場所に置いた人や、もともと似た設定がある人は、適宜変更してください。

```json:tsconfig.json
...
    "paths": {
      ...

      "*": [
        "types/*"
      ]
    },
...
```


次は以下のコードを`src/main.ts`に追記します。
（この追加はvue-async-computedの公式READMEに書かれているものと同じです。）

```ts:src/main.ts
import AsyncComputed from 'vue-async-computed'

Vue.use(AsyncComputed)
```

最後に以下の`AsyncComputed.ts`を好きなところに置きます。
`src/AsyncComputed.ts`に置きました。

```ts:
// (from: https://github.com/foxbenjaminfox/vue-async-computed/issues/25#issuecomment-459369072)

import { createDecorator, VueDecorator } from 'vue-class-component';
import { IAsyncComputedProperty } from 'vue-async-computed';

export function AsyncComputed<T>(
    computedOptions?: IAsyncComputedProperty<T>,
): VueDecorator {
    return createDecorator((_options, key) => {
        // TODO: Not use any casting
        const options: any = _options as any;
        options.asyncComputed = options.asyncComputed || {};
        const method = options.methods![key];
        options.asyncComputed![key] = <IAsyncComputedProperty<T>>{
            get: method,
            ...computedOptions,
        };
        delete options.methods![key];
    });
}
```


あとは、`AsyncComputed`を使えます。
例えば、以下みたいに自分のVueのコンポーネントに`@AsyncComputed`が使えます。

```ts
import { AsyncComputed } from '../AsyncComputed';

@Component
export default class HelloWorld extends Vue {
  @Prop() private msg!: string;

  @AsyncComputed()
  async myAsyncMessage(): Promise<string> {
    return new Promise(resolve => {
      setTimeout(()=>{
        resolve("This is an asynchronous message!");
      }, 1000);
    });
  }
}
```

## おわりに

繰り返しにはなってしまいますが、
以下の**gitの差分をみるのが確実**です。動きも確認できますし、デプロイできてますし、git cloneして、`npm run serve`すればローカルでも試せるはずです。

<https://github.com/nwtgck/typescript-vue-async-computed-prac/commit/ed88abd38452722ff9da346d6f840a965c54812c>


vue-async-computed公式でTypeScript対応してくれると嬉しいですね。