Vue 3とTypeScriptをセットアップして型安全に始める手順 propsとemitsの設計も整理
Vue 3 と TypeScript のセットアップで迷っている方、特に props と emits の型定義方法の違いや型推論を活かした設計判断に悩んでいる方に向けて、実務で検証した手順と判断基準を整理しました。本記事では、初学者でも理解できるセットアップから、実務者が納得できる設計の比較まで、1 記事で完結させます。
propsとemitsの定義方法
| # | 定義方法 | 型推論 | 記述量 | 型安全性 | 実務での採用判断 |
|---|---|---|---|---|---|
| 1 | defineProps<T>() | ○ | 少ない | 高い | ジェネリック型を使う場面で推奨 |
| 2 | withDefaults() | ○ | 中程度 | 高い | デフォルト値が必要な場合に推奨 |
| 3 | ランタイム宣言 | △ | 多い | 中程度 | Vue 2 からの移行時に一時採用 |
| 4 | defineEmits<T>() | ○ | 少ない | 高い | イベント駆動設計で必須 |
| 5 | Tuple syntax | ○ | 中程度 | 高い | イベント引数を厳密に制御したい場合 |
検証環境
- OS: macOS Sequoia 15.2
- Node.js: 24.12.0 (LTS)
- TypeScript: 5.9.3
- 主要パッケージ:
- vue: 3.5.25
- vite: 6.2.1
- @vitejs/plugin-vue: 5.2.1
- 検証日: 2026 年 1 月 9 日
Vue 3とTypeScriptにおけるpropsとemitsの型安全性が重要になる背景
TypeScript導入がVue開発にもたらす変化
Vue 3 では Composition API の導入により、TypeScript との統合が飛躍的に向上しました。特に props と emits の型定義は、コンポーネント間の契約を明示する重要な役割を果たします。Options API では実現が難しかった型推論が、<script setup> と組み合わせることで自然に機能するようになりました。
型推論(Type Inference)とは、開発者が明示的に型を書かなくても、TypeScript が自動的に型を推定してくれる機能です。これにより、冗長なコード記述を減らしつつ、IDE の補完や型チェックの恩恵を受けられます。
しかし、型推論に頼りすぎると、複雑なコンポーネントでは型が曖昧になり、バグの温床になる可能性もあります。実際に検証したところ、props のオプショナルプロパティやemits の引数型を明示しないと、親子コンポーネント間でのデータ受け渡しで実行時エラーが発生しました。
セットアップ手順における設計判断の難しさ
Vue 3 + TypeScript のセットアップでは、以下のような判断が必要になります。
- props の型定義は型推論に任せるべきか、明示的に書くべきか
- emits のイベント名と引数の型はどこまで厳密に定義するべきか
- デフォルト値を持つ props はどう定義するのが最適か
- インターフェース定義はコンポーネント内に書くべきか、別ファイルに切り出すべきか
これらの判断を誤ると、開発初期は問題なくても、コンポーネントが増えるにつれて保守性が低下します。
mermaidflowchart LR
setup["プロジェクトセットアップ"] --> choice["型定義方法の選択"]
choice --> explicit["明示的型定義"]
choice --> inference["型推論"]
explicit --> safe["型安全性<br/>高い"]
inference --> risk["型曖昧化<br/>リスクあり"]
safe --> maintain["保守性向上"]
risk --> bug["実行時エラー"]
上の図は、セットアップ時の型定義方法の選択が、開発後半の保守性に直結することを示しています。初期の判断ミスが後から修正コストを生む典型的なパターンです。
Vue 3 + TypeScript開発で直面する型安全性の課題
propsの型定義における実務上の問題
実際にプロジェクトを進めると、以下のような問題が頻発しました。
問題1: オプショナルプロパティの扱い
typescript// 型推論だけに頼った場合
const props = defineProps({
userId: Number,
userName: String,
});
このコードは一見問題なさそうですが、userId が undefined の可能性を考慮していません。親コンポーネントから props を渡し忘れた場合、実行時エラーが発生します。
問題2: デフォルト値と型の不一致
typescript// デフォルト値が型と合わない例
const props = defineProps({
count: {
type: Number,
default: "0", // 文字列を設定してしまう
},
});
ランタイム宣言では、TypeScript の型チェックが効かず、デフォルト値の型ミスに気づきにくい問題がありました。
emitsの型定義における実務上の問題
問題3: イベント名のタイポ
typescript// emit時にイベント名を間違える
const emit = defineEmits(["update:modelValue"]);
// 実装時に間違える
emit("updateModelValue", newValue); // タイポに気づかない
イベント名が文字列リテラルのみの場合、タイポによるバグが発見しにくくなります。
問題4: イベント引数の型チェック漏れ
typescript// 引数の型が明示されていない
const emit = defineEmits(["user-selected"]);
// 呼び出し時に間違った型を渡してもエラーにならない
emit("user-selected", "invalid-data"); // 本来はUserオブジェクトを渡すべき
実際の業務では、親コンポーネントで受け取る際に型エラーが発生し、原因特定に時間がかかりました。
つまずきポイント
- 型推論だけでは、オプショナルプロパティの扱いが不明確になる
- ランタイム宣言では、TypeScript の型チェックが部分的にしか効かない
- emit のイベント名と引数の型が曖昧だと、実行時エラーの原因になる
propsとemitsの型安全な定義方法と実務での判断基準
propsの型定義方法の比較と選択基準
この章では、props の型定義方法を 3 つ紹介し、それぞれの採用判断を説明します。
方法1: ジェネリック型を使った型定義(推奨)
型推論を最大限に活かしつつ、型安全性を確保する方法です。
typescriptinterface UserProps {
userId: number;
userName: string;
isActive?: boolean;
avatar?: string;
}
const props = defineProps<UserProps>();
採用した理由:
- TypeScript の型チェックが完全に効く
- IDE の補完が強力に機能する
- オプショナルプロパティが明確に定義できる
採用しなかった理由:
- デフォルト値を設定する場合は
withDefaults()が別途必要
方法2: withDefaultsを使ったデフォルト値付き定義(推奨)
デフォルト値が必要な場合は、withDefaults() を組み合わせます。
typescriptinterface ButtonProps {
label: string;
variant?: "primary" | "secondary" | "danger";
size?: "small" | "medium" | "large";
disabled?: boolean;
}
const props = withDefaults(defineProps<ButtonProps>(), {
variant: "primary",
size: "medium",
disabled: false,
});
採用した理由:
- デフォルト値と型定義を両立できる
- オプショナルプロパティのデフォルト値を明示できる
- 型推論が効いたまま、実行時の安全性も確保できる
実際に試したところ、withDefaults() を使うことで、親コンポーネントから props を渡し忘れても、デフォルト値が適用され、実行時エラーを防げました。
方法3: ランタイム宣言(非推奨)
typescriptconst props = defineProps({
userId: {
type: Number,
required: true,
},
userName: {
type: String,
required: true,
},
});
採用しなかった理由:
- TypeScript の型チェックが部分的にしか効かない
- 記述量が多くなる
- 型定義とランタイム検証が分離してしまう
業務で問題になったのは、この方法では IDE の補完が弱く、props の型を間違えても気づきにくかったことです。
emitsの型定義方法の比較と選択基準
方法1: タプル構文を使った厳密な型定義(推奨)
イベント名と引数の型を厳密に定義する方法です。
typescriptinterface User {
id: number;
name: string;
email: string;
}
const emit = defineEmits<{
"user-selected": [user: User];
"user-deleted": [userId: number];
"error-occurred": [error: Error];
"form-submitted": [formData: Record<string, any>];
}>();
採用した理由:
- イベント名のタイポを防げる
- 引数の型が厳密にチェックされる
- IDE の補完が正確に機能する
実際の検証では、親コンポーネントで @user-selected イベントをハンドリングする際、引数の型が自動的に推論され、型安全なイベント処理が実現できました。
方法2: 関数シグネチャ形式(推奨)
より明示的にイベントの型を定義する方法です。
typescriptinterface FormEmits {
(e: "submit", data: FormData): void;
(e: "cancel"): void;
(e: "field-change", field: string, value: any): void;
}
const emit = defineEmits<FormEmits>();
採用した理由:
- イベントごとに関数シグネチャが明確になる
- 複雑なイベント引数を定義しやすい
採用しなかった理由:
- タプル構文の方が直感的で読みやすい
方法3: 文字列配列(非推奨)
typescriptconst emit = defineEmits(["update:modelValue", "change"]);
採用しなかった理由:
- イベント名のタイポに気づけない
- 引数の型チェックが効かない
- IDE の補完が弱い
検証の結果、この方法では実行時エラーが頻発し、デバッグに時間がかかりました。
つまずきポイント
- ジェネリック型を使わないと、型推論の恩恵を受けられない
- withDefaults() を使わないと、デフォルト値と型定義を両立できない
- emit の型定義を省略すると、イベント駆動設計の型安全性が失われる
mermaidflowchart TB
start["props/emits定義"] --> generic{"ジェネリック型<br/>使用?"}
generic -->|Yes| defaults{"デフォルト値<br/>必要?"}
generic -->|No| runtime["ランタイム宣言<br/>型安全性△"]
defaults -->|Yes| withdef["withDefaults()<br/>型安全性◎"]
defaults -->|No| simple["defineProps<T>()<br/>型安全性◎"]
withdef --> safe["型安全な<br/>コンポーネント"]
simple --> safe
runtime --> risk["型チェック<br/>不十分"]
この図は、props 定義時の判断フローを示しています。ジェネリック型を使うことが、型安全性確保の第一歩です。
プロジェクトセットアップから実装までの具体例
Vue 3 + TypeScript プロジェクトの作成手順
動作確認済みの手順を以下に示します。
bash# Viteを使ったプロジェクト作成
npm create vue@latest my-vue-app
# プロジェクト設定での選択
# ✅ TypeScript を選択
# ✅ Router を選択(必要に応じて)
# ✅ ESLint を選択
# ✅ Prettier を選択
# プロジェクトディレクトリに移動
cd my-vue-app
# 依存関係のインストール
npm install
# 開発サーバー起動
npm run dev
この手順で作成すると、TypeScript の設定ファイル(tsconfig.json)が自動生成されます。
tsconfig.jsonの重要な設定項目
生成された tsconfig.json の中で、型安全性に関わる重要な設定を確認します。
json{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
重要な設定項目:
"strict": true: 厳格な型チェックを有効化"noUnusedLocals": true: 未使用の変数を検出"jsx": "preserve": Vue の JSX サポート
実際に試したところ、"strict": true を有効にすることで、props の型定義漏れや emit の引数型の不一致を開発時に検出できました。
型安全なコンポーネントの実装例
基本的なpropsとemitsの定義
vue<!-- src/components/UserCard.vue -->
<script setup lang="ts">
import { computed } from "vue";
// インターフェース定義
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
// props定義(ジェネリック型)
interface UserCardProps {
user: User;
showAvatar?: boolean;
showEmail?: boolean;
}
const props = withDefaults(defineProps<UserCardProps>(), {
showAvatar: true,
showEmail: true,
});
// emits定義(タプル構文)
const emit = defineEmits<{
"user-clicked": [user: User];
"edit-clicked": [userId: number];
"delete-clicked": [userId: number];
}>();
// computed プロパティ(型推論が効く)
const displayName = computed(() => {
return props.user.name || "Unknown User";
});
// イベントハンドラー(型安全)
const handleUserClick = () => {
emit("user-clicked", props.user);
};
const handleEditClick = () => {
emit("edit-clicked", props.user.id);
};
const handleDeleteClick = () => {
emit("delete-clicked", props.user.id);
};
</script>
<template>
<div class="user-card" @click="handleUserClick">
<img
v-if="showAvatar && user.avatar"
:src="user.avatar"
:alt="user.name"
class="avatar"
/>
<div class="user-info">
<h3>{{ displayName }}</h3>
<p v-if="showEmail">{{ user.email }}</p>
</div>
<div class="actions">
<button @click.stop="handleEditClick">編集</button>
<button @click.stop="handleDeleteClick">削除</button>
</div>
</div>
</template>
<style scoped>
.user-card {
display: flex;
align-items: center;
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
margin-right: 16px;
}
.user-info {
flex: 1;
}
.actions {
display: flex;
gap: 8px;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
このコードでは、以下の点を実現しています。
- props の型定義がインターフェースで明確
- デフォルト値が
withDefaults()で設定されている - emit のイベント名と引数の型が厳密に定義されている
- computed プロパティで型推論が効いている
親コンポーネントでの使用例
vue<!-- src/views/UserList.vue -->
<script setup lang="ts">
import { ref } from "vue";
import UserCard from "@/components/UserCard.vue";
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
const users = ref<User[]>([
{
id: 1,
name: "田中太郎",
email: "tanaka@example.com",
avatar: "https://i.pravatar.cc/150?img=1",
},
{
id: 2,
name: "佐藤花子",
email: "sato@example.com",
avatar: "https://i.pravatar.cc/150?img=2",
},
]);
// イベントハンドラー(型推論が効く)
const handleUserClicked = (user: User) => {
console.log("User clicked:", user);
};
const handleEditClicked = (userId: number) => {
console.log("Edit user:", userId);
};
const handleDeleteClicked = (userId: number) => {
users.value = users.value.filter((u) => u.id !== userId);
};
</script>
<template>
<div class="user-list">
<h1>ユーザー一覧</h1>
<UserCard
v-for="user in users"
:key="user.id"
:user="user"
:show-avatar="true"
:show-email="true"
@user-clicked="handleUserClicked"
@edit-clicked="handleEditClicked"
@delete-clicked="handleDeleteClicked"
/>
</div>
</template>
<style scoped>
.user-list {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
margin-bottom: 20px;
}
</style>
親コンポーネントでは、UserCard コンポーネントの props と emits の型が自動的に推論され、IDE の補完が正確に機能します。
つまずきポイント
- インターフェース定義をコンポーネント外に切り出すと、型のインポートが必要になる
- emit のイベント名を
@click.stopで伝播を止めないと、親のイベントも発火する - 配列操作で型推論が効かない場合は、明示的に型を指定する必要がある
型定義ファイルの分離と管理
プロジェクトが大きくなると、型定義を別ファイルに切り出す必要があります。
typescript// src/types/user.ts
export interface User {
id: number;
name: string;
email: string;
avatar?: string;
createdAt: Date;
updatedAt: Date;
}
export interface CreateUserRequest {
name: string;
email: string;
avatar?: string;
}
export interface UpdateUserRequest {
name?: string;
email?: string;
avatar?: string;
}
typescript// src/types/events.ts
import type { User } from "./user";
export interface UserEvents {
"user-selected": [user: User];
"user-created": [user: User];
"user-updated": [user: User];
"user-deleted": [userId: number];
}
コンポーネントで使用する際は、以下のようにインポートします。
vue<script setup lang="ts">
import type { User } from "@/types/user";
import type { UserEvents } from "@/types/events";
const props = defineProps<{
user: User;
}>();
const emit = defineEmits<UserEvents>();
</script>
実際に試したところ、型定義を分離することで、複数のコンポーネント間で型定義を共有でき、保守性が向上しました。
propsとemitsの型定義方法:詳細比較まとめ
ここでは、記事冒頭で示した即答用比較表をより詳細に展開し、実務での判断材料を提供します。
props定義方法の詳細比較
| 定義方法 | 型推論 | IDE補完 | デフォルト値 | 記述量 | 型チェック | 実務での推奨度 |
|---|---|---|---|---|---|---|
defineProps<T>() | ○ | ◎ | △(別途必要) | 少 | 厳格 | ◎(ジェネリック型を使う場面) |
withDefaults(defineProps<T>(), {}) | ○ | ◎ | ○ | 中 | 厳格 | ◎(デフォルト値が必要な場合) |
| ランタイム宣言 | △ | △ | ○ | 多 | 部分的 | △(Vue 2 からの移行時のみ) |
採用判断の基準
ジェネリック型(defineProps<T>())を採用すべきケース:
- props にデフォルト値が不要な場合
- TypeScript の型チェックを最大限活用したい場合
- 複雑な型定義(Union Types、Intersection Types)を使いたい場合
withDefaults を採用すべきケース:
- props にデフォルト値が必要な場合
- オプショナルプロパティのデフォルト値を明示したい場合
- UI コンポーネント(ボタン、フォーム要素)で variant や size を扱う場合
ランタイム宣言を採用しなかった理由:
- TypeScript の型チェックが部分的にしか効かない
- IDE の補完が弱く、開発効率が低下する
- 型定義とランタイム検証が分離し、保守性が低下する
業務で問題になったのは、ランタイム宣言では props の型を間違えても気づきにくく、実行時エラーの原因になることでした。
emits定義方法の詳細比較
| 定義方法 | 型推論 | IDE補完 | イベント名チェック | 引数型チェック | 記述量 | 実務での推奨度 |
|---|---|---|---|---|---|---|
| タプル構文 | ○ | ◎ | ○ | ○ | 少 | ◎(イベント駆動設計で必須) |
| 関数シグネチャ | ○ | ◎ | ○ | ○ | 中 | ○(複雑なイベント引数の場合) |
| 文字列配列 | × | △ | × | × | 少 | ×(型安全性が失われる) |
採用判断の基準
タプル構文を採用すべきケース:
- イベント名と引数の型を厳密に定義したい場合
- IDE の補完を最大限活用したい場合
- イベント駆動設計を採用している場合
関数シグネチャを採用すべきケース:
- イベントごとに複雑な引数を定義したい場合
- より明示的にイベントの型を定義したい場合
文字列配列を採用しなかった理由:
- イベント名のタイポに気づけない
- 引数の型チェックが効かない
- IDE の補完が弱く、開発効率が低下する
検証の結果、タプル構文を使うことで、イベント駆動設計の型安全性が大幅に向上しました。
型推論 vs 明示的型定義の判断基準
| ケース | 型推論を活かす | 明示的に型を書く | 理由 |
|---|---|---|---|
| 単純な props | ○ | △ | 型推論で十分 |
| 複雑な props | △ | ○ | 明示的な方が意図が明確 |
| デフォルト値あり | ○ | ○ | withDefaults で両立 |
| イベント引数 | × | ○ | 明示的定義が必須 |
| computed | ○ | △ | 型推論が効きやすい |
型推論を活かすべきケースと、明示的に型を書くべきケースを正しく判断することが、型安全な開発の鍵になります。
向いているケース / 向かないケース
ジェネリック型 + 型推論が向いているケース:
- 新規プロジェクト
- TypeScript の知識があるチーム
- 型安全性を最優先する開発
ランタイム宣言が向かないケース:
- 型安全性を重視する開発
- 大規模プロジェクト
- チーム開発(型の意図が伝わりにくい)
実際に試したところ、ジェネリック型を使った定義方法が、開発効率と型安全性のバランスが最も良いと判断しました。
まとめ
Vue 3 と TypeScript のセットアップにおいて、props と emits の型定義方法は開発の型安全性を左右する重要な判断ポイントです。本記事では、以下の点を実務検証に基づいて整理しました。
重要なポイント
props の型定義
- ジェネリック型(
defineProps<T>())を使うことで、型推論と型安全性を両立できる - デフォルト値が必要な場合は、
withDefaults()を組み合わせる - ランタイム宣言は、TypeScript の型チェックが部分的にしか効かないため非推奨
emits の型定義
- タプル構文を使うことで、イベント名と引数の型を厳密に定義できる
- 文字列配列のみの定義は、型安全性が失われるため非推奨
- イベント駆動設計では、emit の型定義が必須
セットアップの判断基準
- 型推論を活かしつつ、複雑な部分は明示的に型を書く
- インターフェース定義は、プロジェクトの規模に応じて分離する
tsconfig.jsonの"strict": trueを有効にして、厳格な型チェックを行う
実際に検証したところ、これらの判断基準に従うことで、開発時の型エラーを大幅に減らし、実行時エラーのリスクを最小化できました。
ただし、すべてのケースで型推論が最適とは限りません。プロジェクトの規模、チームの TypeScript 習熟度、UI/UX 要件に応じて、柔軟に判断することが重要です。型安全性を追求しつつ、開発効率とのバランスを取ることが、実務での成功につながります。
関連リンク
著書
article2026年1月9日Vue 3とTypeScriptをセットアップして型安全に始める手順 propsとemitsの設計も整理
articleVue.js 可観測性:Sentry/OpenTelemetry/Web Vitals で UX を数値化
articleVue.js クリーンアーキテクチャ:Composable・サービス層・依存逆転の型
articleVue.js Router 速見表:ガード・遅延ロード・トランジションの定石
articleVue.js Monorepo 構築:pnpm/Turborepo でアプリとパッケージを一元管理
articleVue.js ルーター戦略比較:ネスト/動的セグメント/ガードの設計コスト
article2026年1月13日TypeScriptで既存コードを型安全化する使い方 段階的リファクタリング手順とチェックポイント
article2026年1月13日PlaywrightとTypeScriptでテスト自動化を運用する 型安全な設計と保守の要点
article2026年1月13日TypeScriptでHigher Kinded Typesを模倣する設計 ジェネリクスで関数型パターンを整理
article2026年1月13日Viteで画像とアセット管理をシンプルにする使い方 import運用と構成の考え方
article2026年1月13日TypeScriptで型レベル計算の使い方を学ぶ 算術演算を型システムで実装する
article2026年1月13日TypeScriptで実行時バリデーション自動生成を設計する 型と実行時チェックを整合させる
articleNode.jsセキュリティアップデート、今すぐ必要?環境別の判断フローチャート
articleNode.js HTTP/2サーバーが1リクエストでダウン:CVE-2025-59465の攻撃手法と防御策
articleDatadog・New Relic利用者は要注意:async_hooksの脆弱性がAPMツール経由でDoSを引き起こす理由
articleNext.js・React Server Componentsが危険?async_hooksの脆弱性CVE-2025-59466を徹底解説
article【緊急】2026年1月13日発表 Node.js 脆弱性8件の詳細と対策|HTTP/2・async_hooks のDoS問題を解説
article2026年1月13日TypeScriptで既存コードを型安全化する使い方 段階的リファクタリング手順とチェックポイント
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
