Vue 3 + TypeScript:型安全な Vue 開発入門

現代の Web アプリケーション開発において、TypeScript と Vue 3 の組み合わせは、開発者にとって最強のツールセットの一つとなっています。型安全性を保ちながら、Vue.js の柔軟性と開発効率を最大限に活用できるからです。
本記事では、TypeScript と Vue 3 を組み合わせた開発における型安全性の実現方法を、初心者の方でも段階的に理解できるよう丁寧に解説していきます。基本的な型定義から Composition API での高度な型活用、Props や Emit の型安全性、そして実際の開発で遭遇するエラーの解決方法まで、実践的な知識を体系的にお伝えします。
TypeScript と Vue 3 の相性
なぜ TypeScript と Vue 3 なのか
TypeScript は JavaScript に静的型付けを追加した言語で、コンパイル時にエラーを検出できるため、大規模なアプリケーション開発において非常に重要な役割を果たします。
typescript// TypeScript の基本的な型定義例
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
// 型安全な関数定義
function createUser(userData: Omit<User, 'id'>): User {
return {
id: Math.random(),
...userData,
};
}
// コンパイル時に型チェックが実行される
const newUser = createUser({
name: '田中太郎',
email: 'tanaka@example.com',
isActive: true,
});
Vue 3 では、Composition API の導入により TypeScript との統合が飛躍的に向上しました。Options API と比較して、型推論がより効果的に働くようになっています。
Vue 3 での TypeScript サポートの進化
Vue 3 における TypeScript サポートの改善点を表で整理しました。
# | 改善点 | Vue 2 での課題 | Vue 3 での解決 |
---|---|---|---|
1 | Composition API | Options API での型推論の限界 | 関数ベースで自然な型推論 |
2 | Props の型定義 | 複雑な型定義の記述 | defineProps<T>() での簡潔な型定義 |
3 | Emit の型安全性 | イベント名や引数の型チェック不備 | defineEmits<T>() での完全な型安全性 |
4 | Template の型推論 | テンプレート内での型チェック制限 | より厳密なテンプレート型チェック |
5 | 開発者体験 | 型エラーの分かりにくいメッセージ | 明確で理解しやすいエラーメッセージ |
プロジェクトセットアップ
Vue 3 + TypeScript プロジェクトの立ち上げから始めましょう。
bash# Vue 3 + TypeScript プロジェクトの作成
yarn create vue@latest vue3-typescript-app
# プロジェクト設定での選択
# ✅ TypeScript を選択
# ✅ Router を選択(必要に応じて)
# ✅ ESLint を選択
# ✅ Prettier を選択
# プロジェクトディレクトリに移動
cd vue3-typescript-app
# 依存関係のインストール
yarn install
# 開発サーバー起動
yarn dev
プロジェクト構成では、以下のような TypeScript 関連ファイルが自動生成されます。
typescript// tsconfig.json の重要な設定
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
型定義の基礎
基本的な型定義パターン
Vue 3 アプリケーションでよく使用される型定義パターンを学習しましょう。
typescript// src/types/user.ts - ユーザー関連の型定義
export interface User {
id: number;
name: string;
email: string;
avatar?: string; // オプショナルプロパティ
createdAt: Date;
updatedAt: Date;
}
// ユーザー作成時の型(idと日付フィールドを除外)
export type CreateUserRequest = Omit<
User,
'id' | 'createdAt' | 'updatedAt'
>;
// ユーザー更新時の型(部分的な更新を許可)
export type UpdateUserRequest = Partial<
Pick<User, 'name' | 'email' | 'avatar'>
>;
// API レスポンスの型定義
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
errors?: string[];
}
// ページネーション付きレスポンス
export interface PaginatedResponse<T> {
items: T[];
totalCount: number;
currentPage: number;
totalPages: number;
hasNext: boolean;
hasPrevious: boolean;
}
Union Types と Literal Types
Vue コンポーネントでよく使用される Union Types と Literal Types の活用例です。
typescript// src/types/common.ts - 共通型定義
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'danger'
| 'warning';
export type Size = 'small' | 'medium' | 'large';
export type LoadingState =
| 'idle'
| 'loading'
| 'success'
| 'error';
// 状態管理で使用するAction Types
export type UserAction =
| { type: 'FETCH_USERS_START' }
| { type: 'FETCH_USERS_SUCCESS'; payload: User[] }
| { type: 'FETCH_USERS_ERROR'; payload: string }
| { type: 'CREATE_USER'; payload: CreateUserRequest }
| {
type: 'UPDATE_USER';
payload: { id: number; data: UpdateUserRequest };
}
| { type: 'DELETE_USER'; payload: number };
// コンポーネントで使用するイベント型
export interface ComponentEvents {
'user-selected': [user: User];
'loading-changed': [isLoading: boolean];
'error-occurred': [error: Error];
'form-submitted': [formData: CreateUserRequest];
}
Generic Types の活用
再利用可能な型定義を作成するために Generic Types を活用しましょう。
typescript// src/types/api.ts - 汎用的なAPI型定義
export interface BaseEntity {
id: number;
createdAt: Date;
updatedAt: Date;
}
// ジェネリック型を使用した汎用的なAPI関数型
export type ApiFunction<TRequest, TResponse> = (
request: TRequest
) => Promise<ApiResponse<TResponse>>;
// CRUD操作の型定義
export interface CrudOperations<T extends BaseEntity> {
getAll: () => Promise<ApiResponse<T[]>>;
getById: (id: number) => Promise<ApiResponse<T>>;
create: (
data: Omit<T, keyof BaseEntity>
) => Promise<ApiResponse<T>>;
update: (
id: number,
data: Partial<Omit<T, keyof BaseEntity>>
) => Promise<ApiResponse<T>>;
delete: (id: number) => Promise<ApiResponse<void>>;
}
// フォームの状態管理用型
export interface FormState<T> {
data: T;
errors: Partial<Record<keyof T, string>>;
isSubmitting: boolean;
isValid: boolean;
}
// バリデーションルールの型定義
export type ValidationRule<T> = (value: T) => string | null;
export type ValidationRules<T> = Partial<
Record<keyof T, ValidationRule<any>[]>
>;
Composition API での型活用
ref と reactive の型安全な使用
Composition API での状態管理において、型安全性を保ちながら効率的にコードを書く方法を学習しましょう。
vue<!-- src/components/UserProfile.vue -->
<template>
<div class="user-profile">
<h2>ユーザープロフィール</h2>
<div v-if="loading" class="loading">読み込み中...</div>
<div v-else-if="error" class="error">
エラー: {{ error }}
</div>
<div v-else-if="user" class="user-info">
<img
:src="user.avatar"
:alt="user.name"
class="avatar"
/>
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<p>登録日: {{ formatDate(user.createdAt) }}</p>
<button @click="updateProfile" :disabled="isUpdating">
{{ isUpdating ? '更新中...' : 'プロフィール更新' }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import {
ref,
reactive,
computed,
onMounted,
watch,
} from 'vue';
import type { User, UpdateUserRequest } from '@/types/user';
// Props の型定義
interface Props {
userId: number;
showAvatar?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showAvatar: true,
});
// Emits の型定義
interface Emits {
(e: 'user-loaded', user: User): void;
(e: 'user-updated', user: User): void;
(e: 'error', error: string): void;
}
const emit = defineEmits<Emits>();
// 型安全な ref の使用
const user = ref<User | null>(null);
const loading = ref<boolean>(false);
const error = ref<string | null>(null);
const isUpdating = ref<boolean>(false);
// reactive での複雑な状態管理
const formState = reactive<{
data: UpdateUserRequest;
errors: Record<string, string>;
touched: Record<string, boolean>;
}>({
data: {
name: '',
email: '',
avatar: '',
},
errors: {},
touched: {},
});
// 型安全な computed プロパティ
const isFormValid = computed<boolean>(() => {
return (
Object.keys(formState.errors).length === 0 &&
formState.data.name.length > 0 &&
formState.data.email.length > 0
);
});
const displayName = computed<string>(() => {
return user.value?.name || 'Unknown User';
});
// 型安全な関数定義
const fetchUser = async (id: number): Promise<void> => {
loading.value = true;
error.value = null;
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`
);
}
const userData: User = await response.json();
user.value = userData;
// フォームデータの初期化
Object.assign(formState.data, {
name: userData.name,
email: userData.email,
avatar: userData.avatar || '',
});
emit('user-loaded', userData);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Unknown error';
error.value = errorMessage;
emit('error', errorMessage);
} finally {
loading.value = false;
}
};
const updateProfile = async (): Promise<void> => {
if (!user.value || !isFormValid.value) return;
isUpdating.value = true;
try {
const response = await fetch(
`/api/users/${user.value.id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formState.data),
}
);
if (!response.ok) {
throw new Error(`Update failed: ${response.status}`);
}
const updatedUser: User = await response.json();
user.value = updatedUser;
emit('user-updated', updatedUser);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Update failed';
error.value = errorMessage;
emit('error', errorMessage);
} finally {
isUpdating.value = false;
}
};
// 型安全な watch の使用
watch(
() => props.userId,
(newId: number) => {
if (newId) {
fetchUser(newId);
}
},
{ immediate: true }
);
// ユーティリティ関数
const formatDate = (date: Date): string => {
return new Intl.DateTimeFormat('ja-JP', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(new Date(date));
};
// ライフサイクルフック
onMounted(() => {
console.log('UserProfile component mounted');
});
</script>
<style scoped>
.user-profile {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.loading,
.error {
text-align: center;
padding: 20px;
}
.error {
color: #e74c3c;
background-color: #fdf2f2;
border-radius: 4px;
}
.user-info {
text-align: center;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
margin-bottom: 10px;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
</style>
composable 関数の型定義
再利用可能な composable 関数を型安全に作成する方法です。
typescript// src/composables/useApi.ts
import { ref, reactive } from 'vue';
import type { ApiResponse } from '@/types/api';
export interface UseApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
export interface UseApiReturn<T> {
state: UseApiState<T>;
execute: () => Promise<void>;
reset: () => void;
}
export function useApi<T>(
apiCall: () => Promise<ApiResponse<T>>
): UseApiReturn<T> {
const state = reactive<UseApiState<T>>({
data: null,
loading: false,
error: null,
});
const execute = async (): Promise<void> => {
state.loading = true;
state.error = null;
try {
const response = await apiCall();
if (response.success) {
state.data = response.data;
} else {
state.error = response.message || 'API call failed';
}
} catch (err) {
state.error =
err instanceof Error
? err.message
: 'Unknown error';
} finally {
state.loading = false;
}
};
const reset = (): void => {
state.data = null;
state.loading = false;
state.error = null;
};
return {
state,
execute,
reset,
};
}
typescript// src/composables/useForm.ts
import { reactive, computed } from 'vue';
import type {
ValidationRules,
FormState,
} from '@/types/api';
export interface UseFormOptions<T> {
initialData: T;
validationRules?: ValidationRules<T>;
}
export interface UseFormReturn<T> {
formState: FormState<T>;
updateField: <K extends keyof T>(
field: K,
value: T[K]
) => void;
validate: () => boolean;
reset: () => void;
submit: (
onSubmit: (data: T) => Promise<void>
) => Promise<void>;
}
export function useForm<T extends Record<string, any>>(
options: UseFormOptions<T>
): UseFormReturn<T> {
const formState = reactive<FormState<T>>({
data: { ...options.initialData },
errors: {},
isSubmitting: false,
isValid: false,
});
const updateField = <K extends keyof T>(
field: K,
value: T[K]
): void => {
formState.data[field] = value;
// フィールド単位のバリデーション
if (options.validationRules?.[field]) {
const rules = options.validationRules[field];
const errors: string[] = [];
for (const rule of rules) {
const error = rule(value);
if (error) {
errors.push(error);
}
}
if (errors.length > 0) {
formState.errors[field] = errors[0];
} else {
delete formState.errors[field];
}
}
formState.isValid =
Object.keys(formState.errors).length === 0;
};
const validate = (): boolean => {
if (!options.validationRules) return true;
formState.errors = {};
for (const [field, rules] of Object.entries(
options.validationRules
)) {
const value = formState.data[field as keyof T];
for (const rule of rules) {
const error = rule(value);
if (error) {
formState.errors[field as keyof T] = error;
break;
}
}
}
formState.isValid =
Object.keys(formState.errors).length === 0;
return formState.isValid;
};
const reset = (): void => {
formState.data = { ...options.initialData };
formState.errors = {};
formState.isSubmitting = false;
formState.isValid = false;
};
const submit = async (
onSubmit: (data: T) => Promise<void>
): Promise<void> => {
if (!validate()) return;
formState.isSubmitting = true;
try {
await onSubmit(formState.data);
} catch (error) {
console.error('Form submission error:', error);
throw error;
} finally {
formState.isSubmitting = false;
}
};
return {
formState,
updateField,
validate,
reset,
submit,
};
}
Props・Emit の型安全性
Props の高度な型定義
Vue 3 では defineProps
を使用して、より厳密で表現力豊かな Props の型定義が可能です。
vue<!-- src/components/DataTable.vue -->
<script setup lang="ts">
import { computed } from 'vue';
// 高度な Props 型定義
interface Column<T> {
key: keyof T;
label: string;
sortable?: boolean;
formatter?: (value: any) => string;
width?: string;
}
interface DataTableProps<T extends Record<string, any>> {
data: T[];
columns: Column<T>[];
loading?: boolean;
emptyMessage?: string;
sortBy?: keyof T;
sortOrder?: 'asc' | 'desc';
selectable?: boolean;
selectedItems?: T[];
}
// Props のデフォルト値と型定義を同時に行う
const props = withDefaults(
defineProps<DataTableProps<any>>(),
{
loading: false,
emptyMessage: 'データがありません',
sortOrder: 'asc',
selectable: false,
selectedItems: () => [],
}
);
// Emits の型定義
interface DataTableEmits<T> {
(e: 'sort', column: keyof T, order: 'asc' | 'desc'): void;
(e: 'select', items: T[]): void;
(e: 'row-click', item: T, index: number): void;
}
const emit = defineEmits<DataTableEmits<any>>();
// 型安全な computed プロパティ
const sortedData = computed(() => {
if (!props.sortBy) return props.data;
return [...props.data].sort((a, b) => {
const aValue = a[props.sortBy!];
const bValue = b[props.sortBy!];
if (aValue < bValue)
return props.sortOrder === 'asc' ? -1 : 1;
if (aValue > bValue)
return props.sortOrder === 'asc' ? 1 : -1;
return 0;
});
});
const handleSort = (column: Column<any>): void => {
if (!column.sortable) return;
const newOrder =
props.sortBy === column.key && props.sortOrder === 'asc'
? 'desc'
: 'asc';
emit('sort', column.key, newOrder);
};
const handleRowClick = (item: any, index: number): void => {
emit('row-click', item, index);
};
</script>
<template>
<div class="data-table">
<table>
<thead>
<tr>
<th v-if="selectable" class="select-column">
<input type="checkbox" />
</th>
<th
v-for="column in columns"
:key="String(column.key)"
:style="{ width: column.width }"
:class="{
sortable: column.sortable,
active: sortBy === column.key,
}"
@click="handleSort(column)"
>
{{ column.label }}
<span
v-if="
column.sortable && sortBy === column.key
"
class="sort-indicator"
>
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td
:colspan="columns.length + (selectable ? 1 : 0)"
>
読み込み中...
</td>
</tr>
<tr v-else-if="sortedData.length === 0">
<td
:colspan="columns.length + (selectable ? 1 : 0)"
>
{{ emptyMessage }}
</td>
</tr>
<tr
v-else
v-for="(item, index) in sortedData"
:key="index"
@click="handleRowClick(item, index)"
>
<td v-if="selectable">
<input type="checkbox" />
</td>
<td
v-for="column in columns"
:key="String(column.key)"
>
{{
column.formatter
? column.formatter(item[column.key])
: item[column.key]
}}
</td>
</tr>
</tbody>
</table>
</div>
</template>
カスタムイベントの型安全性
親子コンポーネント間でのイベント通信を型安全に行う方法です。
vue<!-- src/components/UserForm.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import { useForm } from '@/composables/useForm';
import type {
User,
CreateUserRequest,
ValidationRule,
} from '@/types/user';
// Props の型定義
interface UserFormProps {
mode: 'create' | 'edit';
initialData?: Partial<User>;
loading?: boolean;
}
const props = withDefaults(defineProps<UserFormProps>(), {
loading: false,
});
// 厳密な Emits 型定義
interface UserFormEmits {
(e: 'submit', data: CreateUserRequest): void;
(e: 'cancel'): void;
(
e: 'field-change',
field: keyof CreateUserRequest,
value: string
): void;
(
e: 'validation-error',
errors: Record<string, string>
): void;
}
const emit = defineEmits<UserFormEmits>();
// バリデーションルールの定義
const emailRule: ValidationRule<string> = (
value: string
) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value)
? null
: '有効なメールアドレスを入力してください';
};
const nameRule: ValidationRule<string> = (
value: string
) => {
return value.length >= 2
? null
: '名前は2文字以上で入力してください';
};
// フォーム状態の管理
const { formState, updateField, validate, submit, reset } =
useForm<CreateUserRequest>({
initialData: {
name: props.initialData?.name || '',
email: props.initialData?.email || '',
avatar: props.initialData?.avatar || '',
},
validationRules: {
name: [nameRule],
email: [emailRule],
},
});
// フォーム送信処理
const handleSubmit = async (): Promise<void> => {
try {
await submit(async (data) => {
emit('submit', data);
});
} catch (error) {
emit('validation-error', formState.errors);
}
};
// フィールド変更の監視
const handleFieldChange = <
K extends keyof CreateUserRequest
>(
field: K,
value: CreateUserRequest[K]
): void => {
updateField(field, value);
emit('field-change', field, String(value));
};
const handleCancel = (): void => {
reset();
emit('cancel');
};
</script>
<template>
<form @submit.prevent="handleSubmit" class="user-form">
<h3>
{{
mode === 'create' ? 'ユーザー作成' : 'ユーザー編集'
}}
</h3>
<div class="field">
<label for="name">名前 *</label>
<input
id="name"
type="text"
:value="formState.data.name"
@input="
handleFieldChange(
'name',
($event.target as HTMLInputElement).value
)
"
:class="{ error: formState.errors.name }"
required
/>
<span
v-if="formState.errors.name"
class="error-message"
>
{{ formState.errors.name }}
</span>
</div>
<div class="field">
<label for="email">メールアドレス *</label>
<input
id="email"
type="email"
:value="formState.data.email"
@input="
handleFieldChange(
'email',
($event.target as HTMLInputElement).value
)
"
:class="{ error: formState.errors.email }"
required
/>
<span
v-if="formState.errors.email"
class="error-message"
>
{{ formState.errors.email }}
</span>
</div>
<div class="field">
<label for="avatar">アバター URL</label>
<input
id="avatar"
type="url"
:value="formState.data.avatar"
@input="
handleFieldChange(
'avatar',
($event.target as HTMLInputElement).value
)
"
/>
</div>
<div class="form-actions">
<button
type="submit"
:disabled="
!formState.isValid ||
formState.isSubmitting ||
loading
"
>
{{
formState.isSubmitting || loading
? '保存中...'
: mode === 'create'
? '作成'
: '更新'
}}
</button>
<button
type="button"
@click="handleCancel"
:disabled="formState.isSubmitting || loading"
>
キャンセル
</button>
</div>
</form>
</template>
よくあるエラーと解決方法
エラー 1: Type 'string | undefined' is not assignable to type 'string'
bashType 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
原因: Optional Properties や undefined の可能性を考慮していない
typescript// ❌ エラーが発生するコード
interface User {
id: number;
name: string;
email?: string; // オプショナルプロパティ
}
const user: User = {
id: 1,
name: 'John Doe',
};
// email は undefined の可能性があるため、エラーが発生
const emailLength: number = user.email.length;
typescript// ✅ 正しいコード - 型ガードの使用
const emailLength: number = user.email?.length ?? 0;
// または、より明示的な型チェック
const emailLength: number = user.email
? user.email.length
: 0;
// Non-null assertion operator(確実に値がある場合のみ)
const emailLength: number = user.email!.length;
エラー 2: Cannot find name 'defineProps'. Did you mean 'props'?
bashCannot find name 'defineProps'. Did you mean 'props'?
原因: Vue 3 Composition API の設定が正しくない
vue<!-- ❌ エラーが発生するコード -->
<script lang="ts">
import { defineProps } from 'vue';
const props = defineProps<{ message: string }>();
</script>
vue<!-- ✅ 正しいコード -->
<script setup lang="ts">
// setup 属性を使用し、defineProps をインポートしない
const props = defineProps<{ message: string }>();
</script>
typescript// vite.config.ts での設定確認
export default defineConfig({
plugins: [vue()],
define: {
__VUE_OPTIONS_API__: false,
__VUE_PROD_DEVTOOLS__: false,
},
});
エラー 3: Property 'value' does not exist on type 'EventTarget | null'
bashProperty 'value' does not exist on type 'EventTarget | null'.
原因: イベントハンドラーでの型アサーションが不適切
vue<!-- ❌ エラーが発生するコード -->
<template>
<input @input="handleInput" />
</template>
<script setup lang="ts">
const handleInput = (event: Event): void => {
// EventTarget には value プロパティが存在しない
const value = event.target.value;
};
</script>
vue<!-- ✅ 正しいコード -->
<template>
<input @input="handleInput" />
</template>
<script setup lang="ts">
const handleInput = (event: Event): void => {
// 型アサーションを使用
const target = event.target as HTMLInputElement;
const value = target.value;
// または、より安全な方法
if (event.target instanceof HTMLInputElement) {
const value = event.target.value;
}
};
// 型定義を使用したより良い解決方法
const handleInput = (event: InputEvent): void => {
const target = event.target as HTMLInputElement;
const value = target.value;
};
</script>
エラー 4: Argument of type 'unknown' is not assignable to parameter
bashArgument of type 'unknown' is not assignable to parameter of type 'User'.
原因: API レスポンスの型が適切に定義されていない
typescript// ❌ エラーが発生するコード
const fetchUser = async (id: number): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
const userData = await response.json(); // unknown 型
return userData; // エラー
};
typescript// ✅ 正しいコード - 型ガードの使用
const isUser = (obj: any): obj is User => {
return (
obj &&
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string'
);
};
const fetchUser = async (id: number): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
const userData: unknown = await response.json();
if (isUser(userData)) {
return userData;
}
throw new Error('Invalid user data received from API');
};
// または、ライブラリを使用した解決方法(例: zod)
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().transform((str) => new Date(str)),
updatedAt: z.string().transform((str) => new Date(str)),
});
const fetchUserWithValidation = async (
id: number
): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
const userData: unknown = await response.json();
return UserSchema.parse(userData);
};
エラー 5: Module '"*.vue"' has no exported member
bashModule '"./UserComponent.vue"' has no exported member 'UserComponent'.
原因: Vue ファイルの型定義が正しく設定されていない
typescript// ❌ エラーが発生するコード
import { UserComponent } from './UserComponent.vue';
typescript// ✅ 正しいコード
import UserComponent from './UserComponent.vue';
// または、名前付きエクスポートを使用する場合
// UserComponent.vue 内で
export { default as UserComponent } from './UserComponent.vue';
typescript// env.d.ts ファイルの確認(Vite プロジェクト)
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
エラー 6: Type instantiation is excessively deep and possibly infinite
bashType instantiation is excessively deep and possibly infinite.
原因: 循環参照や過度に複雑な型定義
typescript// ❌ エラーが発生するコード
interface DeepNested<T> {
value: T;
nested: DeepNested<DeepNested<T>>; // 過度にネストした型
}
typescript// ✅ 正しいコード - 型の簡略化
interface SimpleNested<T> {
value: T;
children?: SimpleNested<T>[];
}
// または、型パラメータの制限
interface LimitedDepth<T, D extends number = 3> {
value: T;
nested?: D extends 0
? never
: LimitedDepth<T, [-1, 0, 1, 2][D]>;
}
まとめ
Vue 3 と TypeScript を組み合わせた開発における型安全性の実現について、基礎から実践的な活用方法まで学習してきました。
重要なポイント:
型安全性の基礎
- TypeScript の型システムを活用することで、開発時のエラーを大幅に削減
- Vue 3 の Composition API により、より自然で強力な型推論が可能
- Generic Types や Union Types を使った柔軟な型定義
Composition API での型活用
ref
とreactive
の適切な型定義- computed プロパティと watch での型安全性
- 再利用可能な composable 関数の型設計
Props と Emit の型安全性
defineProps
とdefineEmits
での厳密な型定義- イベントハンドリングでの型チェック
- 親子コンポーネント間の型安全な通信
エラー対策とデバッグ
- よくあるエラーパターンの理解と解決方法
- 型ガードや型アサーションの適切な使用
- API 連携での型安全性の確保
TypeScript と Vue 3 の組み合わせは、単に型安全性を提供するだけでなく、開発者体験の向上、保守性の向上、チーム開発でのコミュニケーション改善など、多くのメリットをもたらします。
型定義は最初は複雑に感じるかもしれませんが、段階的に学習し、実際のプロジェクトで活用することで、その価値を実感できるでしょう。エラーの早期発見、IDE での強力な補完機能、リファクタリングの安全性など、TypeScript の恩恵を最大限に活用して、より安全で効率的な Vue アプリケーション開発を実現してください。
関連リンク
- article
【対処法】Cursorで発生する「You've saved $102 on API model usage this month with Pro...」エラーの原因と対応
- article
Vue.js で作るモダンなフォームバリデーション
- article
Jest で setTimeout・setInterval をテストするコツ
- article
Playwright MCP でクロスリージョン同時テストを実現する
- article
Tailwind CSS と Alpine.js で動的 UI を作るベストプラクティス
- article
Storybook を Next.js プロジェクトに最短で導入する方法
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
- review
人類はなぜ地球を支配できた?『サピエンス全史 上巻』ユヴァル・ノア・ハラリが解き明かす驚愕の真実
- review
え?世界はこんなに良くなってた!『FACTFULNESS』ハンス・ロスリングが暴く 10 の思い込みの正体