Vetur で Vue.extend() 使用時に data の中身を推論するには computed, methods あるいは props 定義が必要っぽい (2018 年 11 月時点)

# Vue.extend() 使用時の data 推論には computed などの定義が必要っぽい

[Type Vue without TypeScript](https://blog.usejournal.com/type-vue-without-typescript-b2b49210f0b) の JSDoc による戻り値などの型指定をやっていたところ `$data` についてつまづいたので
`Visual Studio Code` により [Vue CLI 3](https://cli.vuejs.org/) や [Nuxt.js 2](https://ja.nuxtjs.org/) などで試しました。

## `Vue.extend()` による `$data` の推論に必要と思われる `options` は・・・

### `computed` と `methods`

Vue Forum の投稿の
[Vue2.5 でthisが推論されない](https://forum.vuejs.org/t/vue2-5-this/19731) の `computed` のあたりを参考にしました。

```vue:data-inferred-by-computed-and-methods
<script>
import Vue from 'vue';
export default Vue.extend({
  data() {
    return {
      name: 'taro',
    };
  },
  // computed は中身が通常の関数、アロー関数どちらでも問題なし
  // ただし定義をしない、あるいは空にすると $data 配下が any 扱いに
  computed: {
    computedName() {
      return this.name
    },
  },
  mounted() {
    // (property) name: string
    this.name;
  },
  // methods は空でも定義があればよく、
  // 中身の定義をする場合でも通常の関数、アロー関数どちらでもよい
  methods: {
    test: () => 1,
    exec() {},
  },
});
</script>
```

#### あまりよくない computed

投稿時に下記のような実例としたが `Symbol()` により `computed` を定義すると
`data` 配下の要素をそのまま返すような `computed` で使用したプロパティ自体が `any` となる

```vue:symbol-function
<script>
import Vue from 'vue';
export default Vue.extend({
  ・・・
  computed: {
    [Symbol()]() {
      return 0;
    },
    // [Symbol()]: () => 0,
    
    // 以下の事例においては any とはならず定義した型が推論の結果となる
    // aaa: () => 0,
    // [`aaa`]: () => 0,
    computedName() {
      return this.name
    },
  },
  mounted() {
    // computedName() を定義する前までは string として扱われていた
    // any
    this.name
  },
  ・・・
});
</script>
```

### `props` で `default` 定義にアロー関数が指定されているもの

```vue:data-inferred-by-props-arrow-function
<script>
import Vue from 'vue';
export default Vue.extend({
  props: {
    puroppu: {
      type: Boolean,
      default: () => false,
      // アロー関数でないと $data 配下が any になるだけ
      // default() {
      //   return false;
      // },
    },
  },
  data() {
    return {
      name: 'saburo',
    };
  },
  mounted() {
    // (property) ComponentOptions<Vue, DefaultData<Vue>, DefaultMethods<Vue>, DefaultComputed, PropsDefinition<Record<string, any>>, Record<string, any>>.name: string
    this.name;
  },
});
</script>
```

踏み込んで調べてはいないですが `props` 配下にて `default` をアロー関数で指定したものが存在していると
コメントに記載したような `(property) ComponentOptions<Vue, DefaultData<Vue>, DefaultMethods<Vue>, DefaultComputed, PropsDefinition<Record<string, any>>, Record<string, any>>.name: string` という推論？が行われ
結果 `string` が取得できるようです。

かなり長い表示がされるので前述の `computed` `methods` のほうを使いたいと感じましたが
どうも `props` 配下に 1 つでも `default` にアロー関数を使用しているとこちらの方式が優先されるもようです。

## computed まわり

### 十分なプロパティ指定をしないと `data` 配下をそのまま返すような `computed` の型は推論されない

`十分な指定は` したのほうをさんしょう

```vue:not-inferred-computed
<script>
import Vue from 'vue';
export default Vue.extend({
  ・・・
  computed: {
    computedName() {
      // メソッド内部では正しく推論されるが
      // (property) name: string
      return this.name
    },
  },
  mounted() {
    // 使用時に推論されない
    // any
    this.computedName
  },
  ・・・
});
</script>
```

#### `computed` を推論させるために

2 種類の方法が考えられる模様です

* 型が明確な記述の使用
* [Type Vue without TypeScript](https://blog.usejournal.com/type-vue-without-typescript-b2b49210f0b) の事例の通り JSDoc による `@returns` などの指定

  * `@returns` での指定
  * 処理中において `@type` により型の明示された変数を戻り値として指定

```vue:inferred-computed
<script>
import Vue from 'vue';
export default Vue.extend({
  ・・・
  computed: {
    /**
     * @returns {string}
     */
    computedName() {
      return this.name
    },
    typedName() {
      /** @type {string} */
      const name = this.name
      return name
    },
    fullName() {
      return `sato ${this.name}`
    },
    // String のメソッドを使用しても推論されない模様
    trimmedName() {
      // (method) String.trim(): string
      return this.name.trim()
    },
  },
  mounted() {
    // (property) computedName: string
    this.computedName
    // (property) typedName: string
    this.typedName
    // (property) fullName: string
    this.fullName
    // (property) trimmedName: any
    this.trimmedName
  },
  ・・・
});
</script>
```

## props の推論

`props` に指定した要素を推論させる場合
`data`, `computed` それぞれ推論可能な状態でさらに `props` への注釈が必要である模様

* 直接型を指定する
* `type`, `default` などを指定した構造の場合注釈として `constructor` などの指定

```vue:inferred-props
<script>
import Vue from 'vue';
class Entity {
  id = 1
  name = 'entetei'
  constructor() {}
}
export default Vue.extend({
  ・・・
  props: {
    /*
     * マウスオーバーにより表示される推論の結果での type を @type への指定とする
     * (property) propNum: {
     *     type: NumberConstructor;
     *     default(): number;
     * }
     */
    propNum: {
      type: Number,
      default() {
        return 1
      },
    },
    // type に Number や String などの指定をした場合
    /** @type {NumberConstructor} */
    propId: {
      type: Number,
      default() {
        return 1
      },
    },
    // 直接 Number などを指定した場合
    propNo: Number,
    // 独自に定義した場合の注釈
    /** @type {{ new (): Entity }} */
    propEntity: {
      type: Entity,
      default() {
        return new Entity()
      },
    },
  },
  mounted() {
    // (property) propId: number
    this.propId
    // (property) propNo: number
    this.propNo
    // (property) propEntity: Entity
    this.propEntity
  },
  ・・・
});
</script>
```

## とりあえず推論される記述

### props, data, computed のセット

```vue:inferred-template

<script>
import Vue from 'vue'
class Entity {
  id = 1
  name = 'entetei'
  constructor() {}
}

export default Vue.extend({
  props: {
    /** @type {NumberConstructor} */
    propId: {
      type: Number,
      default() {
        return 1
      },
    },
    propNo: Number,
    /** @type {{ new (): Entity }} */
    propEntity: {
      type: Entity,
      default() {
        return new Entity()
      },
    },
    // props に関数を指定する場合 default の戻り値でなく、default自体が初期値となるもよう
    /** @type {FunctionConstructor} */
    propFunction: {
      type: Function,
      default() {
        return 1;
      },
    },
  },
  data() {
    return {
      name: 'taro',
    }
  },
  computed: {
    entity: () => Entity,
    /**
     * @returns {string}
     */
    computedName() {
      return this.name
    },
    typedName() {
      /** @type {string} */
      const name = this.name
      return name
    },
    fullName() {
      return `sato ${this.name}`
    },
    trimmedName() {
      return this.name.trim()
    },
  },
  mounted() {
    // (property) propId: number
    this.propId
    // (property) propNo: number
    this.propNo
    // (property) propEntity: Entity
    this.propEntity
    // (property) propFunction: Function
    this.propFunction
    // (property) name: string
    this.name
    // (property) computedName: string
    this.computedName
    // (property) typedName: string
    this.typedName
    // (property) fullName: string
    this.fullName
    // props も含めて定義されていると data の直接の推論がきくもよう
    // (property) trimmedName: string
    this.trimmedName
  },
  methods: {},
})
</script>
```

### 不要な実装の記述をしたくない場合の JSDoc 利用

JSDoc 使用であれば `props` `computed`, `data` への JSDoc の記述を行い
`methods` を用意してあげればよいようです。

 `props` `computed`, `data` に具体的な実装が入った場合には
記述を修正する必要が発生しますが。

```vue:unknown-options
<script>
import Vue from 'vue';

/**
 * @typedef {Object<string, unknown>} UnknownObject
 */

export default Vue.extend({
  /** @type {UnknownObject} */
  props: {},
  /** @type {{new(): UnknownObject}} */
  data() {
    return {};
  },
  /** @type {UnknownObject} */
  computed: {},
  mounted() {
    // CombinedVueInstance<Vue, new () => {
    //     [x: string]: unknown;
    // }, {}, {
    //     [x: string]: ...;
    // }, Readonly<Record<string, any>>>
    this;
  },
  methods: {},
});
</script>
```