Claude Media
Claude CodeでVue 3アプリを開発する — vue-tscとVitestを検証コマンドにする

Claude CodeでVue 3アプリを開発する — vue-tscとVitestを検証コマンドにする

Vue 3(Vite)をClaude Codeで開発するとき、vue-tscの型検査とVitestをCLAUDE.mdとStop hookで検証手順に固定する方法を、SFC特有の落とし穴とあわせて解説します。

Vue 3のプロジェクトでClaude Codeに「終わった」と判断させる基準は、vue-tsc の型検査と vitest run の2つに絞れます。Viteの開発サーバーは型を検査しないため、画面が動いていても型エラーは残りえます。この記事では、素のVue 3(Vite)を対象に、検証コマンドの決め方、CLAUDE.mdに書くSFC(単一ファイルコンポーネント)の規約、Stop hookで強制する構成を順に説明します。

Viteの開発サーバーは型を見ない

Vue公式のTypeScriptガイドによると、Viteベースの構成では開発サーバーとバンドラーはトランスパイルだけを行い、型検査はしません。TypeScriptを使っても開発サーバーが速いままなのは、このためです。

Claude Codeにとっては、ここが盲点になります。npm run dev でページが表示されたことを根拠に完了報告が返ってきても、<script setup lang="ts"> の型エラーは検出されていません。

SFCの型検査には、コマンドラインの vue-tsc を使います。vue-tsc は tsc のラッパーで、tsc とほぼ同じ使い方のまま .vue ファイルも扱えます。型を明示的に検査するコマンドを、検証手順として固定する必要があります。

隣接する記事のNuxtは、型検査をClaude CodeでNuxtアプリを開発するで扱っています。Nuxt固有の nuxt typecheck や自動インポートの話はそちらにあります。ここでは、Nuxtを使わない素のVue 3に絞ります。

検証コマンドを2本に決める

スキャフォールドツール create-vue でTypeScriptとVitestを選ぶと、package.json には次のスクリプトが入ります。

スクリプトコマンド役割
type-checkコマンドvue-tsc --build役割SFCを含む型検査
test:unitコマンドvitest役割単体・コンポーネントテスト

vue-tsc を単独で導入する場合の最小形は、言語ツールのREADMEにあります。

npm install vue-tsc typescript --save-dev
{
  "scripts": {
    "type-check": "vue-tsc --noEmit"
  }
}

vue-tsc が要求するTypeScriptは5.0.0以上です。READMEの型検査コマンドは vue-tsc --noEmit です。create-vue のテンプレートが --build を使うのは、アプリ用とテスト用でtsconfigを分けるプロジェクト参照の構成だからです。アプリのコードとテストのコードでは使えるグローバル変数が違うため、参照で分ける設計です。既存のプロジェクトでは、すでに type-check スクリプトがあればそれを使い、無ければ上の形から始めます。

テスト側には1つ注意があります。test:unit の中身は引数なしの vitest です。Vitestのドキュメントでは、引数なしの vitest は vitest watch と同じで、監視モードに入ります。CIや標準入力がTTYでない環境では vitest run に切り替わるとされていますが、Claudeに頼むコマンドは vitest run と明示するほうが確実です。

npm run type-check
npx vitest run

どちらも終了コードで成否が分かり、失敗時はエラー出力をClaudeがそのまま読めます。

CLAUDE.mdに書くVue 3の規約

Claude CodeのCLAUDE.mdは、セッション開始時にコンテキストへ読み込まれます。1ファイルあたり200行未満が目安で、長いと遵守率が下がります。検証コマンドと、Claudeが間違えやすい規約だけを書きます。

Vue 3(Vite + TypeScript)向けの例です。ドキュメントにある書式ではなく、Vueの仕様から組み立てた一例です。

# Vue 3プロジェクトの規約
 
## 検証コマンド(完了前に必ず実行し、エラー0にする)
- 型検査: `npm run type-check`(vue-tsc)
- テスト: `npx vitest run`(`vitest` 単体はwatchになるので使わない)
 
## SFCの書き方
- コンポーネントは `<script setup lang="ts">` で書く
  (`lang="ts"` が無いとテンプレートの型検査も効かない)
- propsは型ベース宣言: `defineProps<{ foo: string; bar?: number }>()`
- emitsは `defineEmits<{ change: [id: number] }>()` の形で型を付ける
- propsの既定値は分割代入で書く(Vue 3.4以前は `withDefaults`)
- テンプレート参照は `useTemplateRef` か `ref<T | null>(null)`、
  使うときは `?.` で守る
- 型だけのimportは `import type` を使う
- `@/` エイリアスはtsconfigの `paths` にも定義済み。新しい別名はviteと両方に足す

この例には、SFC特有の落とし穴が5つ入っています。次の節で、それぞれがなぜ型検査で効くのかを見ます。

SFCでvue-tscが拾うもの、拾えないもの

lang="ts" が無いテンプレートは検査されない

<script> に lang="ts" があるとき、テンプレート内の式も厳密に型検査されます。逆に言うと、Claudeが lang="ts" を付け忘れたコンポーネントは、型エラーがあっても vue-tsc を素通りします。新規コンポーネントの作成を頼むときは、規約で lang="ts" を固定します。

テンプレート内の型ミスマッチはvue-tscで出る

Vueのガイドの例では、string | number の変数 x をテンプレートで x.toFixed(2) と呼ぶとエラーになります。回避策はテンプレート内のキャスト (x as number).toFixed(2) です。

エラーを最短で消す手段はキャストですが、キャストは型の嘘を許す書き方です。規約に「まず分岐で絞り込む。キャストは最後の手段」と添えておくと、直し方が安定します。

propsとemitsは型引数で宣言する

型ベースの defineProps<...>() は、型引数から実行時のpropsオプションを生成します。この形は実行時宣言と同じオプションにコンパイルされます。型ベースと実行時宣言を同時に使うことはできません。

制限もあります。型から実行時への変換はASTベースで行われるため、条件付き型のように実際の型解析が要るものは、props全体の型としては使えません。個々のpropの型としてなら使えます。Claudeが凝った型でpropsを定義して失敗したときは、この制限が原因の可能性があります。

emitsは、3.3以降なら change: [id: number] のような簡潔な型引数で書けます。

propsの既定値は書き方がバージョンで変わる

型ベース宣言では既定値を直接書けません。Vue 3.5では、分割代入で書けます。

interface Props {
  msg?: string
  labels?: string[]
}
 
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()

3.4以下ではこの分割代入が既定で有効にならないため、withDefaults を使います。withDefaults ではオブジェクトや配列の既定値を関数で包みます。使っているVueのバージョンをCLAUDE.mdに書いておくと、Claudeが古い書き方と新しい書き方を取り違えません。

テンプレート参照はnullを前提にする

ref で作るテンプレート参照は、マウントされるまで null です。v-if で要素が外れたときも null に戻ります。厳密な型安全のためには、el.value?.focus() のような省略可能チェーンか型ガードが必要です。

Vue 3.5と @vue/language-tools 2.1以降は、useTemplateRef() の型を静的な参照なら自動で推論します。推論できない場合に限り、useTemplateRef<HTMLInputElement>('el') のように型引数で補います。

vue-tscが拾えないもの

型検査が保証するのは、型の整合性までです。ボタンを押したら値が増える、という振る舞いは検査できません。ここからはVitestの担当です。

Vitestでコンポーネントとcomposableを検証する

Vue公式のテストガイドは、create-vue ベースの構成ではVitestを推奨しています。Viteの設定と変換パイプラインをそのまま使えるためです。Jestは、既存のJestのテスト群を移行する場合に限って勧められています。

コンポーネントは公開インターフェースで検証する

コンポーネントテストの指針は、実装の詳細ではなく、発火するイベント・props・スロットといった公開インターフェースを検査することです。「何をするかを検査し、どう実現しているかは検査しない」と表現されています。子コンポーネントはモックせず、ユーザーが操作するようにクリックして確かめる形が勧められています。

マウントには @vue/test-utils が勧められています。下に例を示します。ドキュメントのコードそのものではなく、方針に沿った一例です。

import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import Counter from './Counter.vue'
 
describe('Counter', () => {
  it('ボタンを押すと数が増える', async () => {
    const wrapper = mount(Counter, { props: { initial: 1 } })
    await wrapper.get('button').trigger('click')
    expect(wrapper.text()).toContain('2')
  })
})

DOMを使うテストでは、environment の設定が必要です。Vitestの既定は node で、jsdom と happy-dom を選べます。happy-dom は高速とされる一方で、一部のAPIが足りません。公式のVueガイドは happy-dom を、create-vue のテンプレートは jsdom を使っています。ファイルごとに切り替えたいときは、ファイル先頭に // @vitest-environment jsdom というコメントを書きます。

globalsを使うならtsconfigにも型を足す

Vueのテストガイドは、globals: true を設定して describe や test をimportせずに使う場合、TypeScriptでは tsconfig.json の types に vitest/globals を足すよう説明しています。足さないと vue-tsc が test を見つけられず、テストファイルで型エラーになります。直し方は globals を外すことではなく、types を足すことです。規約に1行入れておきます。

composableはホストコンポーネントの要否で分ける

composableは2種類に分かれます。ライフサイクルフックやProvide / Injectを使わず、リアクティビティAPIだけのものは、関数として直接呼んで戻り値を検査できます。useCounter() を呼んで count.value が0であることを確かめる形です。フックやProvide / Injectを使うものは、ホストコンポーネントで包む必要があります。

どちらの形で書くかはcomposableの中身で決まります。依頼文でどちらに当たるかを伝えるか、規約に「ライフサイクルフックを使うcomposableは createApp を使うヘルパーで包む」と書いておきます。

変更した範囲だけ回す

全体を毎回回す必要はありません。vitest related <ファイル> は、指定したソースファイルを読み込むテストだけを実行します。静的importはたどれますが、import(filepath) のような動的importは対象外です。ファイルはルートからの相対パスで指定するとされています。

Vitestの出力を絞る設定や、related をhookに組み込む詳細はClaude CodeでVitestを回すが扱っています。

Stop hookで型エラーとテスト失敗を残さない

CLAUDE.mdの指示は、あくまでコンテキストです。確実に実行したい検証はhookにします。ここでは、Claudeが応答を終えようとしたときに発火するStop hookで、型検査とテストを両方走らせます。

.claude/settings.json に次を追加します。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/vue-verify.sh"
          }
        ]
      }
    ]
  }
}

スクリプトの例です。

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
 
if ! OUT=$(npm run type-check 2>&1); then
  echo "$OUT" | tail -n 40 >&2
  exit 2
fi
if ! OUT=$(npx vitest run 2>&1); then
  echo "$OUT" | tail -n 40 >&2
  exit 2
fi
exit 0

終了コード2を返すと、Claudeの停止がブロックされて会話が続きます。stop_hook_active の確認が要るのは、Claude Codeが、Claudeのツール呼び出しを挟まないまま8回連続でStopをブロックした時点で、そのhookを上書きして停止させるからです。公式のhookガイドは、スクリプト側で継続済みかを確かめて早期に終了するよう説明しています。

この確認には裏面があります。入れておくと、継続後の2回目の停止要求では検査せずに通します。保証できるのは「終わる前に1度は失敗を突きつける」ことで、エラー0そのものではありません。

型検査を先に走らせるのは、型が通らない状態では、テストの失敗が型の不整合に由来するのか振る舞いの不具合なのか区別しにくくなるためです。個別ファイルの編集直後にLintを回す設計や、テスト自動実行の一般論はClaude Codeのhooksでテストを自動実行する方法にまとまっています。

依頼文の例

規約とhookを入れたうえで、依頼文は結果の確認方法まで含めます。

ユーザー一覧のコンポーネントを src/components/UserList.vue に追加して。
propsは型ベースで宣言し、選択時に select イベントをemitして。
対応するテストを同じ階層に書いて、
npm run type-check と npx vitest run が通るまで直して。

エラー出力はClaudeが直接読めるため、人間が転記しなくても修正が回ります。

よくあるつまずき

症状原因の候補対処
ページは動くのにCIで型エラーが出る原因の候補開発サーバーは型検査しない対処完了条件に type-check を入れる
テンプレートの型エラーが出ない原因の候補<script> に lang="ts" が無い対処規約で lang="ts" を固定する
テストで test is not defined 系の型エラー原因の候補globals を使うのに types が未設定対処tsconfigの types に vitest/globals を足す
テストコマンドが終わらない原因の候補引数なしの vitest で監視モードに入った対処vitest run に揃える
propsの型引数が通らない原因の候補条件付き型などを型引数に使った対処個々のpropの型として使うか、型を単純にする
Options APIで this が any になる原因の候補strict が無効対処compilerOptions.strict を有効にする
@/ のimportが解決されない原因の候補tsconfigの paths に別名が無い対処viteの別名と paths の両方に足す

Options APIの行は、「Options APIを使うなら strict(少なくとも noImplicitThis)を有効にする」という注意に対応します。<script setup> 中心のプロジェクトでは出にくい症状です。

Nuxtや他の検証ループとの使い分け

Nuxtでは、自動インポートの型を生成する nuxt prepare が検証の起点になり、nuxt typecheck が vue-tsc を呼び出します。素のVue 3は自動インポートが無い分、vue-tsc を直接呼ぶ構成で済みます。Nuxtの手順はClaude CodeでNuxtアプリを開発するを参照してください。

ブラウザでの動作まで確かめたい場合は、Vitestの外にE2Eを置く選択肢があります。Vueの公式ガイドは、ブラウザ型のランナーはスタイルや実際のDOMイベントを検出できる代わりに、Vitestより桁違いに遅いと説明しています。PlaywrightをhookでE2E実行する構成はClaude CodeでPlaywrightのE2Eテストを自動実行するにあります。

検証コマンドを完了条件に据える考え方は、言語が変わっても共通です。Goなら go test を軸にしたClaude CodeでGoアプリを開発する手順が同じ形で、Vue 3では vue-tsc と vitest run がその役を担います。

この記事を共有:XはてブLinkedIn