T-CREATOR

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

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

現代の Web アプリケーション開発において、TypeScriptVue 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 での解決
1Composition APIOptions API での型推論の限界関数ベースで自然な型推論
2Props の型定義複雑な型定義の記述defineProps<T>() での簡潔な型定義
3Emit の型安全性イベント名や引数の型チェック不備defineEmits<T>() での完全な型安全性
4Template の型推論テンプレート内での型チェック制限より厳密なテンプレート型チェック
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 での型活用

  • refreactive の適切な型定義
  • computed プロパティと watch での型安全性
  • 再利用可能な composable 関数の型設計

Props と Emit の型安全性

  • definePropsdefineEmits での厳密な型定義
  • イベントハンドリングでの型チェック
  • 親子コンポーネント間の型安全な通信

エラー対策とデバッグ

  • よくあるエラーパターンの理解と解決方法
  • 型ガードや型アサーションの適切な使用
  • API 連携での型安全性の確保

TypeScript と Vue 3 の組み合わせは、単に型安全性を提供するだけでなく、開発者体験の向上、保守性の向上、チーム開発でのコミュニケーション改善など、多くのメリットをもたらします。

型定義は最初は複雑に感じるかもしれませんが、段階的に学習し、実際のプロジェクトで活用することで、その価値を実感できるでしょう。エラーの早期発見、IDE での強力な補完機能、リファクタリングの安全性など、TypeScript の恩恵を最大限に活用して、より安全で効率的な Vue アプリケーション開発を実現してください。

関連リンク