1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue 3 × TypeScriptで始めるアプリケーション開発入門

1
Posted at

Vue 3 × TypeScriptで始めるアプリケーション開発入門

はじめに

Vue 3とTypeScriptを組み合わせることで、シンプルな記述を保ちながら、型安全なフロントエンドアプリケーションを開発できます。

Vue 3ではComposition APIや<script setup>が利用でき、従来よりもコンポーネントのロジックを整理しやすくなりました。

この記事では、Vue 3とTypeScriptを使って簡単なTodoアプリを作成しながら、以下の内容を説明します。

  • Vue 3プロジェクトの作成
  • TypeScriptの基本的な使い方
  • refcomputed
  • 型定義
  • コンポーネント分割
  • PropsとEmit
  • フォーム入力の処理

開発環境

今回使用する主な技術は以下です。

  • Vue 3
  • TypeScript
  • Vite
  • Node.js
  • npm

プロジェクトを作成する

まず、Viteを使用してVue 3のプロジェクトを作成します。

npm create vite@latest vue3-typescript-todo

実行すると、フレームワークと使用言語を選択する画面が表示されます。

以下のように選択します。

Framework: Vue
Variant: TypeScript

プロジェクトのディレクトリに移動します。

cd vue3-typescript-todo

必要なパッケージをインストールします。

npm install

開発サーバーを起動します。

npm run dev

ブラウザで表示されたURLへアクセスし、Vueの初期画面が表示されれば準備完了です。

Todoアプリの完成イメージ

今回作成するTodoアプリでは、次の機能を実装します。

  • Todoの追加
  • Todoの完了状態の切り替え
  • Todoの削除
  • 未完了件数の表示

型を定義する

TypeScriptでは、アプリケーション内で扱うデータの構造を型として定義できます。

src/types/Todo.tsを作成します。

export type Todo = {
  id: number
  title: string
  completed: boolean
}

この型を使うことで、Todoデータには必ず以下の値が含まれることを保証できます。

{
  id: number
  title: string
  completed: boolean
}

例えば、titleに数値を代入すると、TypeScriptがエラーを検出します。

const todo: Todo = {
  id: 1,
  title: 100,
  completed: false,
}

titleは文字列として定義されているため、上記のコードは型エラーになります。

App.vueを作成する

src/App.vueを以下のように編集します。

<script setup lang="ts">
import { computed, ref } from 'vue'
import type { Todo } from './types/Todo'

const newTodoTitle = ref<string>('')

const todos = ref<Todo[]>([
  {
    id: 1,
    title: 'Vue 3を学習する',
    completed: false,
  },
  {
    id: 2,
    title: 'TypeScriptを学習する',
    completed: true,
  },
])

const remainingCount = computed<number>(() => {
  return todos.value.filter((todo) => !todo.completed).length
})

const addTodo = (): void => {
  const title = newTodoTitle.value.trim()

  if (!title) {
    return
  }

  const newTodo: Todo = {
    id: Date.now(),
    title,
    completed: false,
  }

  todos.value.push(newTodo)
  newTodoTitle.value = ''
}

const toggleTodo = (id: number): void => {
  const todo = todos.value.find((item) => item.id === id)

  if (!todo) {
    return
  }

  todo.completed = !todo.completed
}

const deleteTodo = (id: number): void => {
  todos.value = todos.value.filter((todo) => todo.id !== id)
}
</script>

<template>
  <main class="container">
    <h1>Todoアプリ</h1>

    <form class="todo-form" @submit.prevent="addTodo">
      <input
        v-model="newTodoTitle"
        type="text"
        placeholder="Todoを入力してください"
      />

      <button type="submit">
        追加
      </button>
    </form>

    <p>未完了: {{ remainingCount }}</p>

    <ul class="todo-list">
      <li
        v-for="todo in todos"
        :key="todo.id"
        class="todo-item"
      >
        <label>
          <input
            type="checkbox"
            :checked="todo.completed"
            @change="toggleTodo(todo.id)"
          />

          <span :class="{ completed: todo.completed }">
            {{ todo.title }}
          </span>
        </label>

        <button
          type="button"
          @click="deleteTodo(todo.id)"
        >
          削除
        </button>
      </li>
    </ul>
  </main>
</template>

<style scoped>
.container {
  width: min(600px, 90%);
  margin: 40px auto;
  font-family: sans-serif;
}

.todo-form {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
}

.todo-form input {
  flex: 1;
  padding: 10px;
}

.todo-form button {
  padding: 10px 16px;
}

.todo-list {
  padding: 0;
  list-style: none;
}

.todo-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 0;
  border-bottom: 1px solid #ddd;
}

.completed {
  color: #888;
  text-decoration: line-through;
}
</style>

<script setup lang="ts">とは

Vue 3では、次のように記述することでComposition APIを簡潔に利用できます。

<script setup lang="ts">
</script>

setupを指定すると、setup()関数を明示的に書かなくても、定義した変数や関数をテンプレートから利用できます。

lang="ts"を指定すると、script内でTypeScriptを使用できます。

従来の書き方では、次のようにsetup()関数とreturnが必要でした。

export default {
  setup() {
    const message = ref('Hello')

    return {
      message,
    }
  },
}

<script setup>では、以下のように簡潔に書けます。

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

const message = ref<string>('Hello')
</script>

refでリアクティブな値を定義する

Vueでは、画面の状態として変化する値をリアクティブなデータとして管理します。

const newTodoTitle = ref<string>('')

この例では、入力フォームの値を文字列として管理しています。

TypeScriptの型は、ref<string>のように指定できます。

const count = ref<number>(0)
const message = ref<string>('')
const isLoading = ref<boolean>(false)

JavaScript側で値を参照したり変更したりする場合は、.valueを使用します。

newTodoTitle.value = 'TypeScriptを学習する'

一方、Vueのテンプレート内では.valueを書く必要はありません。

<p>{{ newTodoTitle }}</p>

配列に型を付ける

Todoの一覧は、Todo型の配列として定義しています。

const todos = ref<Todo[]>([])

Todo[]は、「Todo型のデータが複数入る配列」という意味です。

以下のようなデータは正しく扱えます。

todos.value.push({
  id: 1,
  title: 'Vueを学習する',
  completed: false,
})

一方、必要なプロパティが不足している場合は型エラーになります。

todos.value.push({
  id: 1,
  title: 'Vueを学習する',
})

completedが存在しないため、TypeScriptがエラーを検出します。

computedで計算結果を管理する

未完了のTodo件数は、computedを使用して計算しています。

const remainingCount = computed<number>(() => {
  return todos.value.filter((todo) => !todo.completed).length
})

computedは、依存している値が変更されたときに、自動的に再計算されます。

今回の場合、todosの追加、削除、完了状態の変更が発生すると、未完了件数も自動的に更新されます。

テンプレートでは次のように表示できます。

<p>未完了: {{ remainingCount }}件</p>

フォームの入力値を受け取る

入力フォームでは、v-modelを使用しています。

<input v-model="newTodoTitle" type="text" />

v-modelを使用すると、入力欄の値とVue側の変数を双方向で同期できます。

ユーザーが文字を入力すると、newTodoTitleの値も更新されます。

フォームの送信処理は、次のように記述しています。

<form @submit.prevent="addTodo">

.preventは、フォーム送信時のページ再読み込みを防止するための修飾子です。

Todo追加処理は次のとおりです。

const addTodo = (): void => {
  const title = newTodoTitle.value.trim()

  if (!title) {
    return
  }

  const newTodo: Todo = {
    id: Date.now(),
    title,
    completed: false,
  }

  todos.value.push(newTodo)
  newTodoTitle.value = ''
}

戻り値がない関数には、voidを指定できます。

const addTodo = (): void => {
}

コンポーネントを分割する

アプリケーションが大きくなった場合、すべての処理をApp.vueに書くと管理しにくくなります。

Todoの1行を、別のコンポーネントへ分割してみます。

src/components/TodoItem.vueを作成します。

<script setup lang="ts">
import type { Todo } from '../types/Todo'

defineProps<{
  todo: Todo
}>()

const emit = defineEmits<{
  toggle: [id: number]
  delete: [id: number]
}>()
</script>

<template>
  <li class="todo-item">
    <label>
      <input
        type="checkbox"
        :checked="todo.completed"
        @change="emit('toggle', todo.id)"
      />

      <span :class="{ completed: todo.completed }">
        {{ todo.title }}
      </span>
    </label>

    <button
      type="button"
      @click="emit('delete', todo.id)"
    >
      削除
    </button>
  </li>
</template>

<style scoped>
.todo-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 0;
  border-bottom: 1px solid #ddd;
}

.completed {
  color: #888;
  text-decoration: line-through;
}
</style>

Propsに型を付ける

親コンポーネントから子コンポーネントへ値を渡す場合は、Propsを使用します。

defineProps<{
  todo: Todo
}>()

このコードにより、TodoItemコンポーネントは、Todo型のtodoを受け取ることが明確になります。

親コンポーネントでは次のように渡します。

<TodoItem :todo="todo" />

間違った型の値を渡した場合、TypeScriptやエディターがエラーを検出してくれます。

Emitに型を付ける

子コンポーネントから親コンポーネントへイベントを通知する場合は、Emitを使用します。

const emit = defineEmits<{
  toggle: [id: number]
  delete: [id: number]
}>()

この型定義によって、toggleイベントとdeleteイベントは、数値型のidを引数として渡すことが明確になります。

イベントを送信する処理は次のとおりです。

emit('toggle', todo.id)

存在しないイベント名や、間違った型の引数を渡すと、TypeScriptがエラーを検出します。

App.vueからTodoItemを利用する

コンポーネント分割後のApp.vueは次のようになります。

<script setup lang="ts">
import { computed, ref } from 'vue'
import TodoItem from './components/TodoItem.vue'
import type { Todo } from './types/Todo'

const newTodoTitle = ref<string>('')

const todos = ref<Todo[]>([
  {
    id: 1,
    title: 'Vue 3を学習する',
    completed: false,
  },
])

const remainingCount = computed<number>(() => {
  return todos.value.filter((todo) => !todo.completed).length
})

const addTodo = (): void => {
  const title = newTodoTitle.value.trim()

  if (!title) {
    return
  }

  todos.value.push({
    id: Date.now(),
    title,
    completed: false,
  })

  newTodoTitle.value = ''
}

const toggleTodo = (id: number): void => {
  const todo = todos.value.find((item) => item.id === id)

  if (!todo) {
    return
  }

  todo.completed = !todo.completed
}

const deleteTodo = (id: number): void => {
  todos.value = todos.value.filter((todo) => todo.id !== id)
}
</script>

<template>
  <main class="container">
    <h1>Todoアプリ</h1>

    <form class="todo-form" @submit.prevent="addTodo">
      <input
        v-model="newTodoTitle"
        type="text"
        placeholder="Todoを入力してください"
      />

      <button type="submit">
        追加
      </button>
    </form>

    <p>未完了: {{ remainingCount }}</p>

    <ul class="todo-list">
      <TodoItem
        v-for="todo in todos"
        :key="todo.id"
        :todo="todo"
        @toggle="toggleTodo"
        @delete="deleteTodo"
      />
    </ul>
  </main>
</template>

<style scoped>
.container {
  width: min(600px, 90%);
  margin: 40px auto;
  font-family: sans-serif;
}

.todo-form {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
}

.todo-form input {
  flex: 1;
  padding: 10px;
}

.todo-form button {
  padding: 10px 16px;
}

.todo-list {
  padding: 0;
  list-style: none;
}
</style>

TypeScriptを使用するメリット

Vue 3でTypeScriptを使用する主なメリットは以下です。

型の間違いを早い段階で検出できる

JavaScriptでは、実行して初めて気付くエラーがあります。

TypeScriptを使用すると、コーディング中やビルド時に型の間違いを検出できます。

const count = ref<number>(0)

count.value = '1'

countは数値型として定義されているため、文字列を代入するとエラーになります。

エディターの補完が利用しやすい

型が定義されていることで、Visual Studio Codeなどのエディターから補完候補が表示されます。

todo.title
todo.completed
todo.id

存在するプロパティを確認しながら実装できるため、入力ミスを減らせます。

コンポーネント間のデータ構造が明確になる

PropsとEmitに型を付けることで、親コンポーネントと子コンポーネントの責務が分かりやすくなります。

defineProps<{
  todo: Todo
}>()

このコンポーネントがどのようなデータを必要としているか、コードから判断できます。

リファクタリングしやすい

型定義を変更すると、影響を受ける箇所がエラーとして表示されます。

例えば、Todo型にpriorityを追加した場合、修正が必要な箇所を見つけやすくなります。

export type Todo = {
  id: number
  title: string
  completed: boolean
  priority: 'low' | 'medium' | 'high'
}

Vue 3とTypeScriptを使う際の注意点

必要以上に型を複雑にしない

型安全を意識しすぎて、型定義が複雑になりすぎると、コードが読みにくくなる場合があります。

まずは基本的な型から始めることが重要です。

type User = {
  id: number
  name: string
}

必要になった段階で、Union型やGenericsなどを導入するとよいでしょう。

anyを多用しない

anyを使用すると、TypeScriptの型チェックが無効になります。

const data: any = {}

型が分からない場合でも、可能であればunknownを使用し、値を確認してから処理します。

const data: unknown = 'Vue'

if (typeof data === 'string') {
  console.log(data.toUpperCase())
}

APIレスポンスを信用しすぎない

TypeScriptの型は、実行時のデータを保証するものではありません。

APIから想定外のデータが返される可能性もあります。

type User = {
  id: number
  name: string
}

型を定義していても、実際のAPIレスポンスにnameが存在するとは限りません。

本格的なアプリケーションでは、バリデーションライブラリを使って実行時にデータを検証する方法もあります。

まとめ

Vue 3とTypeScriptを組み合わせることで、Vueの書きやすさを維持しながら、型安全なアプリケーションを開発できます。

今回紹介した重要なポイントは以下です。

  • <script setup lang="ts">でTypeScriptを利用する
  • refでリアクティブな値を管理する
  • computedで計算結果を管理する
  • typeでデータ構造を定義する
  • definePropsでPropsに型を付ける
  • defineEmitsでイベントに型を付ける
  • コンポーネントを適切に分割する
  • anyを多用しない

Vue 3とTypeScriptを学習する際は、最初から大規模なアプリケーションを作るのではなく、Todoアプリのような小さなアプリから始めるのがおすすめです。

基本的なデータ管理、イベント処理、コンポーネント分割を理解した後に、Vue Router、Pinia、API通信などへ進むと、より実践的なアプリケーションを作成できるようになります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?