Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
15
Help us understand the problem. What is going on with this article?

More than 1 year has passed since last update.

@ysKuga

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 などの定義が見れるようになります。

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 は | によりつなげたものでした
15
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
15
Help us understand the problem. What is going on with this article?