Vue.js で JSDoc により実装をせずに data などの型指定をする

# Vue.js で JSDoc により実装をせずに data などの型指定をする

Vue.js で `data` や `props` の中身を書かず、 JSDoc により型定義を行います。
[Type Vue without TypeScript](https://blog.usejournal.com/type-vue-without-typescript-b2b49210f0b) を参考にしています。

## 実例

```vue:jsdoc-js.vue
<script>
// // @ts-check
import Vue from 'vue'

/**
 * @typedef {Object} Props
 * @property {string|number} propId ぷろっぷあいでい
 */
/**
 * @typedef {Object} Data
 * @property {number} id あいでぃい
 * @property {string} name なまえ
 */
/**
 * @typedef {Object} Computed
 * @property {string} computedId けいさんされたあいでい
 */
/**
 * @typedef {Object} Methods
 * @property {{(name): boolean}} checkName なまえをちぇっくするめそっど
 */

export default Vue.extend({
  /** @type {import("vue/types/options").RecordPropsDefinition<Props>} */
  props: {
    propId: {
      type: String,
      default: () => 'aidei',
    },
  },
  /** @typedef {import("vue/types/options").DataDef<Data, Props, Vue>} DataDef */
  /**
   * @type {DataDef}
   * @return {Data}
   */
  data() {
    return {}
  },
  /** @type {import("vue/types/options").Accessors<Computed>} */
  computed: {},
  watch: {
    // WatchOptionsWithHandler だと handler の型指定がうまくいかず、
    // WatchOptionsWithHandler を指定しなくとも deep などはサジェストされるのでこの指定は不要かも
    //* * @type {import('vue/types/options').WatchOptionsWithHandler<Data['id']>} */
    id: {
      // WatchOptionsWithHandler だとなぜか val が any になってしまう・・・
      /** @type {import('vue/types/options').WatchHandler<Data['id']>} */
      handler(val, oldVal) {
        console.log(val, oldVal)
      },
    },
  },
  mounted() {
    this.propId
  },
  /** @type {Methods} */
  methods: {},
})
</script>
```

この例では `props.propId` を除いて `data.id`, `data.name`, `computed.computedId`, `methods.checkName` がそれぞれ「実装された」ことになっています。
※厳密には実装されておらず、使用しようとすると実行時にはエラーが起きます。

そこで VSCode などで見てみると・・・

props
![props.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116435/45e66db0-5589-ff09-4bc1-29b28d20579a.jpeg)

data
![data.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116435/43ff9748-2607-e04b-bed5-1280495e7115.jpeg)

computed
![computed.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116435/7e98d53d-a4df-3824-b313-42daf0e22a9e.jpeg)

methods
![methods.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116435/8bd4bdbd-7684-6835-42e9-47de3880f334.jpeg)

それぞれ型、コメントなどが反映されていることがわかります。
後は実装するだけです。

## いちいち JSDoc の内容と照らし合わせるのがめんどくさい

JSDoc の記述の通り実装するといっても実装の有無をいちいち確認するのは面倒です。
そこで・・・

```vue:ts-check-activate
<script>
// @ts-check
```

最初の行の `//// @ts-check` としていた記述を `// @ts-check` として [TypeScript のチェックを有効化](https://blog.usejournal.com/type-vue-without-typescript-b2b49210f0b#4220) しましょう。

![problems.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116435/40cb3a9c-8e57-7690-386a-7384b7eeff56.jpeg)

エラーがドバーっと出ました。

```
Type '(this: Readonly<Props> & Vue) => {}' is not assignable to type 'DataDef<Data, Props, Vue>'.
  Type '(this: Readonly<Props> & Vue) => {}' is not assignable to type '(this: Readonly<Props> & Vue) => Data'.

Type '(this: Readonly<Props> & Vue) => {}' is not assignable to type 'DataDef<Data, Props, Vue>'.
  Type '(this: Readonly<Props> & Vue) => {}' is not assignable to type '(this: Readonly<Props> & Vue) => Data'.
    Type '{}' is missing the following properties from type 'Data': id, name

Property 'computedId' is missing in type '{}' but required in type 'Accessors<Computed>'.

Property 'checkName' is missing in type '{}' but required in type 'Methods'.
```

それぞれのエラーを潰していく形で実装すれば自然と必要な実装がそろうでしょう・・・。

## 実例 (export default {}) の場合

```vue:jsdoc-export-object-literal.vue
<script>
// // @ts-check
/** @typedef {import("vue").default} Vue */

export default {
  /**
   * @typedef {Object} Props
   * @property {string|number} propId ぷろっぷあいでい
   */
  /** @type {import("vue/types/options").RecordPropsDefinition<Props>} */
  props: {
    propId: {
      type: String,
      default: () => 'aidei',
    },
  },
  /**
   * @typedef {Object} Data
   * @property {number} id あいでぃい
   * @property {string} name なまえ
   */
  /**
   * @typedef {import("vue/types/options").DataDef<Data, Props, Vue>} DataDef
   */
  /**
   * @type {DataDef}
   * @return {Data}
   */
  data() {
    return {};
  },
  /**
   * @typedef {Object} Computed
   */
  /**
   * @type {import("vue/types/options").Accessors<Computed>}
   */
  computed: {},
  watch: {
    id: {
      /** @type {import('vue/types/options').WatchHandler<Data['id']>} */
      handler(val, oldVal) {
        console.log(val, oldVal)
      },
    },
  },
  mounted() {
    this.id
  },
  /**
   * @typedef {Object} Methods
   * @property {{(name): boolean}} checkName なまえをちぇっくするめそっど
   */
  /** @type {Methods} */
  methods: {},
};
</script>
```

## JSDoc の記述をまとめる

とりあえずできたのは `Vue.extend` のほうです。

### `Vue.extend` 使用

```vue:jsdoc-extend-no-import
<script>
// // @ts-check
import Vue from 'vue';
/**
 * @typedef {Object} Props
 * @property {string|number} propId ぷろっぷあいでい
 */
/**
 * @typedef {Object} Data
 * @property {number} id あいでぃい
 * @property {string} name なまえ
 */
/**
 * @typedef {Object} Computed
 * @property {string} computedId けいさんされたあいでい
 */
/**
 * @typedef {Object} Methods
 * @property {{(name): boolean}} checkName なまえをちぇっくするめそっど
 */

// eslint-disable-next-line
/** @typedef {import("vue/types/options").ThisTypedComponentOptionsWithRecordProps<Vue, Data, Methods, Computed, Props>} Options */
/** @type {Options} */
export const options = {
  props: {},
  data() {
    return {};
  },
  computed: {},
  methods: {},
};

export default Vue.extend(options);
</script>
```

#### `Vue.extend` よけいな import

エラーがでるかもです

```bash
 ERROR  Failed to compile with 1 errors

This dependency was not found:

* vue/types/options in ./node_modules/babel-loader/lib??ref--2-0!./node_modules/vue-loader/lib??vue-loader-options!./pages/sample/jsdoc.vue?vue&type=script&lang=js&

To install it, you can run: npm install --save vue/types/options
```

```vue:jsdoc-extend
<script>
// // @ts-check
import Vue from 'vue';
import {
  // eslint-disable-next-line
  ThisTypedComponentOptionsWithRecordProps as Options
} from 'vue/types/options';

/**
 * @typedef {Object} Props
 * @property {string|number} propId ぷろっぷあいでい
 */
/**
 * @typedef {Object} Data
 * @property {number} id あいでぃい
 * @property {string} name なまえ
 */
/**
 * @typedef {Object} Computed
 * @property {string} computedId けいさんされたあいでい
 */
/**
 * @typedef {Object} Methods
 * @property {{(name): boolean}} checkName なまえをちぇっくするめそっど
 */

/** @type {Options<Vue, Data, Methods, Computed, Props>} */
export const options = {
  props: {},
  data() {
    return {};
  },
  computed: {},
  methods: {},
};

export default Vue.extend(options);
</script>
```