nuxt.js + TypeScript でうまくページコンポーネントを定義する

nuxt.js の `/pages/` 以下に置かれる Vue シングルページコンポーネントは特殊で、 `asyncData` や `head` などの追加メソッドが呼び出されます。

また、 plugin で値を inject して拡張することも出来ます。

そのあたりを考慮しつつ TypeScript で上手に型定義が出来ると嬉しいですね。

ということで今使っている型定義を共有してみます。

ここでは [nuxt-property-decorator](https://www.npmjs.com/package/nuxt-property-decorator) を使って記述していますが、 [vue-class-component](https://www.npmjs.com/package/vue-class-component) だけでも動作します(Component しか使っていないので)。

## 型ファイル

`d.ts` 型定義ファイルは、 inject する `Foo` クラスの型も定義しないといけなくて面倒なので使いません。

```ts:src/vue.ts
import { Component, Vue } from 'nuxt-property-decorator'
// 次のバージョンでは @nuxt/typescript の方に型定義が移動する予定です
import { Context, NuxtAppOptions } from '@nuxt/vue-app'
import { Foo } from '~/src/foo'

/**
 * プラグインで `inject` した値
 */
export interface InjectedValues {
  $foo: () => Foo
}

/**
 * nuxt の Context に `inject` した値を追加した型
 */
export interface NuxtAppContext extends Context {
  app: NuxtAppOptions & InjectedValues
}

/**
 * 一番基底となるコンポーネントクラス
 */
@Component
export class BaseVue extends Vue implements InjectedValues {
  // 特に実装はなし
}

/**
 * ページ以外のコンポーネントの基底クラス
 */
@Component
export class ComponentVue extends BaseVue {
  /**
   * head は PageComponentVue のみ定義出来る
   */
  public head!: void

  /**
   * asyncData は PageComponentVue のみ定義出来る
   */
  public asyncData!: void

  /**
   * fetch は PageComponentVue のみ定義出来る
   */
  public fetch!: void
}

/**
 * ページコンポーネントの基底クラス
 */
@Component
export class PageComponentVue extends BaseVue {
  /**
   * meta タグを埋める
   */
  public head(): any {
    // ページタイトルを必須にするため、デフォルトはエラー
    throw new Error(`head メソッドを定義してください`);
  }

  public mounted() {
    // ここではクライアントレンダリング時に全てのページで行う初期化を記述する
  }


  /**
   * ページをレンダリングする前に呼ばれる。戻り値を object にすることで data に値を詰めれる。
   * @param this **この関数内で this は利用出来ない**
   * @param ctx コンテキスト
   */
  public async asyncData(this: void, ctx: NuxtAppContext): Promise<any> {
    // do nothing
  }

  /**
   * ページをレンダリングする前に呼ばれる。
   * @param this **この関数内で this は利用出来ない**
   * @param ctx コンテキスト
   */
  public async fetch(this: void, ctx: NuxtAppContext): Promise<void> {
    // do nothing
  }
}

/**
 * `/layouts` 以下のコンポーネントの基底クラス
 */
@Component
export class LayoutComponentVue extends ComponentVue {
  // 特に実装はなし
}
```

## ページコンポーネントファイル

```ts:pages/index.vue
<template>
  <h1>Index</h1>
</template>

<script lang="ts">
import { Component } from 'nuxt-property-decorator'
import { PageComponentVue, NuxtAppContext } from '~/src/vue'

@Component({
  layout: 'simple',
  // サブコンポーネントがあればここで定義
  // components: { SubComponent, },
  // middleware もここで定義
  // middleware: 'guest',
})
export default class IndexPage extends PageComponentVue {
  public head() {
    return {
      title: 'インデックスページ',
      meta: [
        { hid: 'og:title', name: 'og:title', content: 'インデックスページ' },
      ],
    }
  }

  public async asyncData(this: void, ctx: NuxtAppContext): Promise<any> {
    // this は利用出来ないことが明示される
    // inject した値が利用出来る
    ctx.app.$foo()
  }
}
</script>
```

## 普通のコンポーネントファイル

```ts:components/the-title.vue
<template>
  <h1>{{ title }}</h1>
</template>

<script lang="ts">
import { Component, Prop } from 'nuxt-property-decorator'
import { ComponentVue } from '~/src/vue'

@Component
export default class TheTitle extends ComponentVue {
  @Prop({
    type: String,
    required: true,
  })
  public readonly title!: string
}
</script>
```

---

良い感じに型を使えるようになりました。