Laravel/React経験者がVue3 × TypeScript × Viteに入門してみた(2026年版)
はじめに
普段はLaravel/PHPでバックエンド、フロントはReactを書いています。Vue3を触る機会があったので、Reactとの対応関係を軸にVue3 × TypeScriptの書き方を整理しました。
同じように「Reactは書けるけどVueは未経験」という人が、最短で書き始められることを目指しています。
対象読者:
- JavaScript/TypeScriptの基礎がある
- ReactまたはAngularの経験がある
- Vue3をこれから触る
環境構築
2026年8月時点の安定版は Vue 3.5.x です(3.6はrc段階で、Vapor Modeが目玉)。本記事は3.5系を前提にしています。
npm create vue@latest
対話形式で TypeScript / Vue Router / Pinia / Vitest などを選べます。Viteベースのプロジェクトが生成されるので、create-vue を使っておけば環境構築で悩む要素はほぼありません。
型チェックは vue-tsc が担当します。.vue ファイルの中の型は tsc では見られないため、CIに入れるならこちらです。
{
"scripts": {
"type-check": "vue-tsc --build"
}
}
エディタはVSCodeなら Vue - Official(旧Volar)拡張を入れます。Vetur時代の記憶で入れると動かないので注意。
TypeScriptでVueを書くときの核心
Vue3 × TypeScriptで最初に押さえるべきは、defineProps / defineEmits の型ベース宣言です。ここさえ理解すれば、あとはReactの知識で大半が読めます。
Props
Reactでいう interface Props をジェネリクスで渡します。
<script setup lang="ts">
interface Props {
title: string
count?: number
items: string[]
}
const props = defineProps<Props>()
</script>
デフォルト値が必要な場合、従来は withDefaults を使いました。
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [],
})
Vue 3.5からはProps の分割代入がリアクティブ性を保ったまま使えるようになったので、こちらのほうが素直に書けます。
const { title, count = 0 } = defineProps<Props>()
配列やオブジェクトのデフォルト値もそのまま書けるので、withDefaults のファクトリ関数(() => [])から解放されるのが地味に嬉しいポイントです。
Emits
Reactでコールバックをpropsで渡していた部分が、Vueでは emit になります。3.3以降はタプル構文が使えます。
const emit = defineEmits<{
submit: [payload: { title: string }]
cancel: []
}>()
emit('submit', { title: 'タスク' })
イベント名と引数の型が両方効くので、Reactのコールバックprops以上に型安全な印象を受けました。
v-model
親子で値を双方向に持つ場合、3.4以降は defineModel 一発です。
<script setup lang="ts">
const model = defineModel<string>({ required: true })
</script>
<template>
<input v-model="model" />
</template>
以前は props.modelValue を受け取って emit('update:modelValue') を返す定型文が必要でしたが、それが1行になりました。React経験者からするとcontrolled componentの記述量が減る方向の変更で、素直に便利です。
Composition APIの型
ref / reactive
import { ref, computed } from 'vue'
const count = ref(0) // Ref<number> に推論される
const tasks = ref<Task[]>([]) // 初期値が空なら明示する
const doubled = computed(() => count.value * 2) // ComputedRef<number>
.value の扱いだけは慣れが必要です。<script> 内では必要、<template> 内では自動でアンラップされるので不要、という非対称なルールになっています。ここが最初の躓きポイントでした。
reactive はオブジェクト専用で、分割代入するとリアクティブ性が失われます。迷ったら ref に寄せるのが公式の推奨です。
テンプレート参照
3.5で useTemplateRef が入り、変数名とref名の紐付けが不要になりました。
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const input = useTemplateRef<HTMLInputElement>('search-input')
onMounted(() => input.value?.focus())
</script>
<template>
<input ref="search-input" />
</template>
React対応表
書き換えながら手元に置いていた対応表です。
| React | Vue 3 |
|---|---|
useState |
ref / reactive
|
useMemo |
computed |
useEffect |
watch / watchEffect
|
useEffect(fn, []) |
onMounted |
| クリーンアップ関数 | onUnmounted |
useRef(DOM) |
useTemplateRef |
useContext |
provide / inject
|
| props | defineProps |
| コールバックprops | defineEmits |
children |
<slot> |
{cond && <X />} |
v-if |
{arr.map(...)} |
v-for |
| カスタムフック | Composable(useXxx) |
Composableはカスタムフックとほぼ同じ設計思想で、ref を返す関数を切り出すだけです。依存配列がない分、こちらのほうが書きやすいと感じました。
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
return { count, increment }
}
Pinia(状態管理)
公式の状態管理ライブラリです。setup store構文を使うと、Composition APIとほぼ同じ書き味になります。
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useTaskStore = defineStore('task', () => {
const tasks = ref<Task[]>([])
const completed = computed(() => tasks.value.filter((t) => t.done))
function add(title: string) {
tasks.value.push({ id: crypto.randomUUID(), title, done: false })
}
return { tasks, completed, add }
})
型注釈をほとんど書かずに推論が効くので、Reduxのボイラープレートを知っていると拍子抜けするくらい軽いです。Zustandに近い感覚。
なお、APIからのデータ取得(キャッシュ、重複排除、再検証)はPinia自体のスコープ外です。この領域は Pinia Colada が担当し、2026年初頭に安定版が出ています。React QueryのVue版という位置づけなので、サーバー状態を扱うなら併用を検討する価値があります。
生成AIに書かせるときのハマりどころ
CursorやClaude Codeに書かせながら進めたのですが、Vueは学習データにVue2とOptions APIの記述が大量に混ざっているため、放っておくと古い書き方が出てきます。実際に遭遇したものを挙げます。
-
Options APIが混入する —
export default { data() {...}, methods: {...} }が生成される -
Vue2の記法 —
this.$emit(...)やVue.component(...) -
ランタイムprops宣言に戻る —
defineProps({ title: { type: String, required: true } })と書かれ、型が付かない -
.valueの付け忘れ / 付けすぎ — テンプレート内に.valueを書いてしまう
対策として、プロジェクト直下のルールファイル(CLAUDE.md や .cursorrules)に方針を明記しておくと精度が上がりました。
## Vue コーディング規約
- Vue 3.5系。Composition API + `<script setup lang="ts">` のみを使用する
- Options API、Vue2の記法(this.$emit 等)は使用しない
- propsは型ベース宣言(`defineProps<Props>()`)で書く。ランタイム宣言は禁止
- 双方向バインディングは `defineModel` を使う
- 状態管理はPiniaのsetup store構文を使う
生成されたコードは vue-tsc を通せば型レベルの問題は検出できますが、「動くけど古い書き方」はコンパイルを通ってしまうため、レビュー時に上記のパターンを意識して見るのが現実的だと思います。
まとめ
- Vue3 × TypeScriptの本体は
defineProps/defineEmitsの型ベース宣言。ここを押さえれば大半が読める - 3.5のProps分割代入と
useTemplateRefで、定型文がかなり減っている - ReactのHooksとComposition APIは対応関係が素直なので、React経験者の移行コストは低い
- 生成AIはVue2/Options APIに引っ張られやすいので、ルールファイルでの明示が有効
React経験があるなら、記法の翻訳表を手元に置いて小さいアプリを1本作るのが一番早いと感じました。
参考
- Vue.js 公式ドキュメント(日本語版あり)
- Pinia 公式ドキュメント