TypeScript の Vue.extend(options) で options の型を明示せず data などの同時指定時に props の default がアロー関数でないと型の解釈ができなくなる

# TypeScript の Vue.extend(options) で options の型を明示せず data などの同時指定時に props の default がアロー関数でないと型の解釈ができなくなる

とりあえず試したのは VSCode, Vetur, nuxt-ts の環境です。

## Vue.js での TypeScript 使用の前提

前提として・・・ Vue.js で TypeScript を使用する際には`noImplicitThis` を true にしましょう。
this の推論が効くようになり data や props などの定義が見れるようになります。

```json:tsconfig.json
{
  "compilerOptions": {
    "noImplicitThis": true
  }
}
```

![this の推論](https://qiita-image-store.s3.amazonaws.com/0/116435/7df77eeb-03bd-5801-eed6-17200e66d23e.jpeg)

[TypeScript のサポート - Vue.js](https://jp.vuejs.org/v2/guide/typescript.html)

> コンポーネントメソッド内で this の型をチェックするには strict: true (もしくは最低でも strict フラグの一部の noImplicitThis: true) を含める必要があることに注意してください。

## props の推論の事例

### options に型を指定しないケース

```vue:options-any-props
<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  props: {
    propId: {
      type: Number,
      default() {
        return 1;
      },
    },
  },
  mounted() {
    this.propId;
  },
});
</script>
```

単純に `propId` を定義しており、下記のようにその型が推論されています。

![推論された props](https://qiita-image-store.s3.amazonaws.com/0/116435/c8158377-d027-a1dd-8d95-a3391fd769be.jpeg)

こちらに data を指定すると・・・

```vue:options-any-props-data

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  props: {
    propId: {
      type: Number,
      default() {
        return 1;
      },
    },
  },
  // こちらに data を指定
  data() {
    return {
      name: 'Taro',
    };
  },
  mounted() {
    this.propId;
  },
});
</script>
```

![propId が any に](https://qiita-image-store.s3.amazonaws.com/0/116435/297293dd-fb09-ece8-da7c-bf81b809ef92.jpeg)

`propId` が `any` となりさらにエラー扱いになってしまいました。
一方で data に指定した `name` は正しく解釈されています。

![props は推論されないが data は推論される](https://qiita-image-store.s3.amazonaws.com/0/116435/d8a6b96b-b313-743e-d3ab-8fbe3bc99588.jpeg)

#### props の default をアロー関数にしてみる

これといって根拠がないんですが・・・いじっていたところ動いたケースとして
props の default の指定をアロー関数にしてみます。

```vue:options-any-props-default-arrow-function
<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  props: {
    propId: {
      type: Number,
      // 通常の関数のメソッド指定からアロー関数の指定に
      // default() {
      default: () => {
        return 1;
      },
    },
  },
  data() {
    return {
      name: 'Taro',
    };
  },
  mounted() {
    this.propId;
  },
});
</script>
```

![エラーが解消されて推論された props](https://qiita-image-store.s3.amazonaws.com/0/116435/aa4f1636-a3e7-f559-4aef-24bcb2317b99.jpeg)

先ほどでていたエラーは消え data と併用しつつ `propId` が推論されています。

### options に型を指定してみる

#### options の型に指定する ThisTypedComponentOptionsWithRecordProps

Vue.extend() の定義は `node_modules/vue/types/vue.d.ts` にあり、
引数の指定の仕方によりオーバーロード？をして処理の流れを切り替えているようです。

```ts:node_modules/vue/types/vue.d.ts

export interface VueConstructor<V extends Vue = Vue> {
  new <Data = object, Methods = object, Computed = object, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): CombinedVueInstance<V, Data, Methods, Computed, Record<PropNames, any>>;
  // ideally, the return type should just contain Props, not Record<keyof Props, any>. But TS requires to have Base constructors with the same return type.
  new <Data = object, Methods = object, Computed = object, Props = object>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): CombinedVueInstance<V, Data, Methods, Computed, Record<keyof Props, any>>;
  new (options?: ComponentOptions<V>): CombinedVueInstance<V, object, object, object, Record<keyof object, any>>;

  extend<Data, Methods, Computed, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>;
  extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
  extend<PropNames extends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>;
  extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>;
  extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;
```

`extend` の記述は 5 箇所のようですが・・・これまでの記述を踏襲するとすると
`Props` が `Object` となるような `ThisTypedComponentOptionsWithRecordProps` が適当そうです。
`ThisTypedComponentOptionsWithRecordProps` は `node_modules/vue/types/options.d.ts` に定義があり・・・

```ts:node_modules/vue/types/options.d.ts
/**
 * This type should be used when an object mapped to `PropOptions` is used for a component's `props` value.
 */
export type ThisTypedComponentOptionsWithRecordProps
  // この改行はないですみやすさのためです
  <V extends Vue, Data, Methods, Computed, Props> =
  object &
  ComponentOptions<V, DataDef<Data, Props, V>, Methods, Computed, RecordPropsDefinition<Props>, Props> &
  ThisType<CombinedVueInstance<V, Data, Methods, Computed, Readonly<Props>>>;
```

使用するための型定義として `Vue` に加え、 `Data`, `Methods`, `Computed`, `Props` の `interface` が必要そうです。

#### 実際の型の指定事例

```vue:options-typed
<script lang="ts">
import Vue from 'vue';
import { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options';

interface Data {
  name: string;
}
interface Methods {}
interface Computed {}
interface Props {
  propId: number;
  propAidei: string;
}

const options: ThisTypedComponentOptionsWithRecordProps<
  Vue,
  Data,
  Methods,
  Computed,
  Props
> = {
  props: {
    propId: {
      type: Number,
      default() {
        return 1;
      },
    },
    propAidei: {
      type: String,
      default: () => {
        return 'ichi';
      },
    },
  },
  data() {
    return {
      name: 'Jiro',
    };
  },
  mounted() {
    this.propId;
    this.propAidei;
  },
};

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

`Methods`, `Computed` については空の `interface` にしています。
さらに props の指定に通常の関数、アロー関数それぞれを指定していますが・・・

![通常関数、アロー関数それぞれの props](https://qiita-image-store.s3.amazonaws.com/0/116435/5fe3c4a1-7b30-fd34-dff7-e4f50fd60f23.jpeg)

エラーは発生せず props の推論も正しく行われています。

#### this の型

ちなみに `this` の型はこんな感じになっています。

![this の型](https://qiita-image-store.s3.amazonaws.com/0/116435/17a761ff-304b-b362-2759-262b65228f7a.jpeg)

`interface` でそれぞれ定義された `Props` などが `&` により ~~Union Type~~ Intersection Type になっているようです。

[Intersection Types - Advanced Types - TypeScript](https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types)
~~[Union Types - Advanced Types - TypeScript](https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types)~~

* Union Type は `|` によりつなげたものでした