第10章: TypeScript 導入ガイド

第10章: TypeScript 導入ガイド

概要

TypeScriptはJavaScriptにデータの型付けを追加した言語で、Microsoftによって開発されました。Vue.jsプロジェクトでTypeScriptを導入することで、コードの品質向上、バグの早期発見、IDEのコード補完機能強化など多くのメリットが得られます。Vue 3はTypeScriptでゼロから書き直されており、型のサポートが大幅に改善されています。この章では、Vue.jsプロジェクトにTypeScriptを導入する方法と基本的な使い方について解説します。

1. TypeScriptとは

TypeScriptはJavaScriptの上位集合(スーパーセット)で、静的型付けや型推論などの機能を追加しています。これにより、開発時にエラーを早期に発見でき、コードの可読性や保守性が向上します。すべてのJavaScriptコードは有効なTypeScriptコードですが、TypeScriptはさらに型定義や型注釈などの機能を提供します。

2. Vue.jsプロジェクトでTypeScriptを設定する

既存のVue.jsプロジェクトにTypeScriptを導入する場合と、最初からTypeScriptを使ってプロジェクトを作成する場合の2つのパターンがあります。

2.1 新規プロジェクトでTypeScriptを使用する

npm create vue@latest my-vue-ts-app

# プロジェクト作成時の質問で、TypeScriptを選択:
# ✓ Add TypeScript? Yes

2.2 既存プロジェクトにTypeScriptを追加する

# TypeScriptと関連パッケージをインストール
npm install -D typescript @vue/tsconfig @types/node

次に、プロジェクトルートにtsconfig.jsonファイルを作成します:

{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "strict": true
  }
}

3. Vue ComponentでTypeScriptを使用する

Vue 3では、Composition APIとTypeScriptの相性が非常に良いです。以下の例で基本的な使い方を見てみましょう。

3.1 Script Setup with TypeScript

<script setup lang="ts">
import { ref, computed } from 'vue'

// 型注釈を使用した変数定義
const message = ref<string>('Hello TypeScript')
const count = ref<number>(0)

// 型推論も可能(明示的な型注釈なし)
const doubleCount = computed(() => count.value * 2)

// 関数に型を定義
function increment(): void {
  count.value++
}

// インターフェースの定義
interface User {
  id: number
  name: string
  email?: string // オプショナルプロパティ
}

// インターフェースを使用
const user = ref<User>({
  id: 1,
  name: '山田太郎'
})
</script>

<template>
  <div>
    <h1>{{ message }}</h1>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    
    <div>
      <h2>User Info</h2>
      <p>ID: {{ user.id }}</p>
      <p>Name: {{ user.name }}</p>
    </div>
  </div>
</template>

4. PropsとEmitsの型定義

コンポーネント間の通信では、PropsとEmitsに型を定義することで安全なデータのやり取りが可能になります。

4.1 Propsの型定義

<script setup lang="ts">
// definePropsに型を定義する方法
const props = defineProps<{
  title: string
  likes?: number
  isPublished: boolean
  commentIds: number[]
  author: {
    name: string
    email: string
  }
}>()

// またはインターフェースを使用
interface Author {
  name: string
  email: string
}

interface PostProps {
  title: string
  likes?: number
  isPublished: boolean
  commentIds: number[]
  author: Author
}

// 上記のインターフェースを使用
const propsWithInterface = defineProps<PostProps>()
</script>

4.2 Emitsの型定義

<script setup lang="ts">
// defineEmitsに型を定義
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

// 使用例
function handleChange(id: number) {
  emit('change', id)
}

function handleUpdate(value: string) {
  emit('update', value)
}
</script>

5. TypeScriptでのReactive Stateの型管理

TypeScriptではリアクティブなデータの型を明示的に定義できます。

<script setup lang="ts">
import { reactive, ref, computed } from 'vue'

// refを使う場合
const count = ref<number>(0)
const message = ref<string>('Hello')
const items = ref<string[]>([])

// reactiveを使う場合
interface User {
  id: number
  name: string
  email: string
  isActive: boolean
}

const user = reactive<User>({
  id: 1,
  name: '山田太郎',
  email: 'yamada@example.com',
  isActive: true
})

// computedプロパティの型推論
const userName = computed(() => user.name.toUpperCase())
// 戻り値の型は自動的にstring型として推論される
</script>

6. TypeScriptでのVueルーターの使用

Vue Routerを使用する際も型の恩恵を受けることができます。

// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'

// ルート定義に型を追加
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: AboutView
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router

コンポーネント内でのルーターの使用:

<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'

// 型付きルーターとルートパラメータ
const router = useRouter()
const route = useRoute()

// IDパラメータを取得(型安全)
const id = route.params.id as string

// ルーターのメソッドを使用
function navigateToHome() {
  router.push({ name: 'home' })
}

function navigateToUser(userId: number) {
  router.push({ name: 'user', params: { id: userId.toString() } })
}
</script>

7. TypeScriptでのPiniaの使用

Piniaは最初からTypeScriptをサポートしており、型安全な状態管理を実現できます。

// stores/counter.ts
import { defineStore } from 'pinia'

// インターフェースでストアの状態の型を定義
interface CounterState {
  count: number
  name: string
}

export const useCounterStore = defineStore('counter', {
  state: (): CounterState => ({
    count: 0,
    name: 'Counter'
  }),
  getters: {
    doubleCount(): number {
      return this.count * 2
    }
  },
  actions: {
    increment() {
      this.count++
    },
    updateName(name: string) {
      this.name = name
    }
  }
})

コンポーネント内でのPiniaストアの使用:

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

// ストアを使用
const counterStore = useCounterStore()

// storeToRefsで状態をリアクティブに分解
const { count, name } = storeToRefs(counterStore)

// アクションを使用
function handleIncrement() {
  counterStore.increment()
}

function updateStoreName(newName: string) {
  counterStore.updateName(newName)
}
</script>

<template>
  <div>
    <h2>{{ name }}: {{ count }}</h2>
    <p>Double: {{ counterStore.doubleCount }}</p>
    <button @click="handleIncrement">Increment</button>
    <input v-model="name" placeholder="Store name" />
  </div>
</template>

8. TypeScriptのユーティリティタイプ

TypeScriptには便利なユーティリティタイプが用意されており、これらを活用することでより柔軟な型定義が可能になります。

// よく使われるユーティリティタイプの例

// Partial: すべてのプロパティをオプショナルにする
interface User {
  id: number
  name: string
  email: string
}

function updateUser(userId: number, userUpdate: Partial<User>) {
  // userUpdateはUserの一部のプロパティだけを含む可能性がある
}

// Pick: 特定のプロパティだけを選択
type UserBasicInfo = Pick<User, 'id' | 'name'>
// UserBasicInfo = { id: number; name: string }

// Omit: 特定のプロパティを除外
type UserWithoutEmail = Omit<User, 'email'>
// UserWithoutEmail = { id: number; name: string }

// Record: キーと値の型を指定
type UserRoles = Record<string, boolean>
const roles: UserRoles = {
  admin: true,
  editor: false
}

9. 型の拡張と継承

TypeScriptでは、既存の型を拡張したり継承したりすることができます。

// インターフェースの拡張
interface BaseUser {
  id: number
  name: string
}

interface AdminUser extends BaseUser {
  role: 'admin'
  permissions: string[]
}

// 交差型(Intersection Types)
type Employee = {
  employeeId: string
  department: string
}

type EmployeeUser = BaseUser & Employee
// EmployeeUser = { id: number; name: string; employeeId: string; department: string }

10. 実践的なTips

Vue.jsでTypeScriptを使う上で役立つヒントをいくつか紹介します。

10.1 型定義ファイル(.d.ts)

カスタム型定義ファイルを作成して、プロジェクト全体で使用できる型を定義できます。

// src/types/index.d.ts
export interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
}

export interface Post {
  id: number
  title: string
  content: string
  authorId: number
  created: Date
}

10.2 環境変数の型定義

Viteの環境変数を型安全にするには、環境変数の宣言ファイルを作成します。

// env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string
  readonly VITE_APP_TITLE: string
  // その他の環境変数
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

10.3 Vue Componentのタイプチェック

コンポーネントの実装が正しいかを型チェックするために、TypeScriptの厳格モードを有効にすることをお勧めします。

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

まとめ

TypeScriptはVue.jsプロジェクトに型安全性をもたらし、コードの品質と保守性を向上させます。この章では、Vue.jsプロジェクトでのTypeScriptの基本的な使い方を紹介しました。実際のプロジェクトでは、徐々に型定義を導入していくことで、段階的にTypeScriptの恩恵を受けることができます。Vue 3とComposition APIはTypeScriptとの相性が良いため、両者を組み合わせることで、より堅牢なアプリケーション開発が可能になります。

演習問題

  1. 既存のVueコンポーネントをTypeScriptに変換してみましょう。
  2. Propsに複雑な型定義を追加し、そのProps型を使用するコンポーネントを作成してください。
  3. PiniaストアをTypeScriptで実装し、それを使用するコンポーネントを作成してください。
  4. ユーティリティタイプを使って、既存の型から新しい型を作成する例を3つ考えてください。
目次に戻る