LoginSignup
15

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-03-09

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

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

Vue.js での TypeScript 使用の前提

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

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

this の推論

TypeScript のサポート - Vue.js

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

props の推論の事例

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

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

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

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 に

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

props は推論されないが data は推論される

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

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

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

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

options に型を指定してみる

options の型に指定する ThisTypedComponentOptionsWithRecordProps

Vue.extend() の定義は node_modules/vue/types/vue.d.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 箇所のようですが・・・これまでの記述を踏襲するとすると
PropsObject となるような ThisTypedComponentOptionsWithRecordProps が適当そうです。
ThisTypedComponentOptionsWithRecordPropsnode_modules/vue/types/options.d.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, Propsinterface が必要そうです。

実際の型の指定事例

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

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

this の型

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

this の型

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

Intersection Types - Advanced Types - TypeScript
Union Types - Advanced Types - TypeScript

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15