🐊

【Vue.js 3】TypeScriptで始めるVue3

2023/06/26に公開

初めに

Vue2のサポートが2023年12月31日に終了となり、これからVueを使用する際はVue3がほとんどだと思います。
私自身、最近はReactを扱うことが多くVueも2系の経験しかなかったのですが実務でVue3を扱うことになり、私と同じように手っ取り早く概要をキャッチアップしたい人向けに本記事を作成しました✏️
Vue自体がTypeScriptで書かれており、TypeScriptサポートを提供していますので、TypeScriptで始めます🎉

想定読者

  • Vue2の実務経験がある
  • TypeScriptの学習経験がある
  • Vue3の学習を始めたいので手っ取り早く環境構築や簡単な概要を掴みたい

TypeScript未学習者は下記記事も見てみてください!

https://zenn.dev/knm/articles/30c7013dc61fe5

環境構築

https://ja.vuejs.org/guide/typescript/overview.html

create-vue は公式のプロジェクトセットアップツールで、Vite を用いた TypeScript 対応の Vue プロジェクトをセットアップするためのオプションを提供します。
現在、Vue CLI 経由で Vue 3 + TypeScript を使っている場合、Vite への移行を強く推奨します。

Vue CLIはTypeScriptのサポートしていますが、推奨はしていませんので
Viteをベースにしたcreate-vueで環境構築をしていきます。
ちなみにVitaVueも、アジア出身のEvan You(エヴァン・ヨー)という方が開発しています。
天才エンジニアですね🙌

npm create vue@3

上記コマンドを実行すると、色々と質問されますので回答していけば雛形の作成が完了します。

cd プロジェクト
npm install
npm run dev

上記コマンドでローカルサーバーが立ち上がるかと思います🎉
秒で終わりますね。

拡張機能インストール

Vue3 × TypeScript 開発に欠かせないVScode拡張機能の紹介です🛠

Vue Language Features (Volar)

VolarはVue.jsの開発をサポートする拡張機能です。
.vueファイルのシンタックスハイライトやインテリセンスによる補完ができるようになります。
詳細は、Volar GitHubリポジトリを参照してください。

https://marketplace.visualstudio.com/items?itemName=Vue.volar

TypeScript Vue Plugin (Volar)

TSファイル内で*.vueをインポートする際に型サポートを得る場合、TypeScript Vue Pluginも必要となります。

https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin

フォルダ構成

creat-vueで生成されたフォルダ構成は主に下記の通りです。

フォルダ・ファイル名 内容
dist/ npm run build実行で生成されるディレクトリ。トランスパイル + バンドル + コンパイルされたもの。
src/ 単一コンポーネントやページコンポーネントなど主に開発で編集するものを格納
└ assest/ cssや画像
└ componets/ 単一コンポーネント
└ router/ ルーディングファイル
└ views/ ページコンポーネント
└ App.vue トップのコンポーネント
└ main.ts エントリーポイント
env.d.ts 環境定義ファイル
tsconfig.json TypeScriptの設定ファイル
vite.config.ts Viteの設定ファイル

CompositionAPI

Vue3から追加された内容で一番の特徴はCompositionAPIの導入です。
Composition APIはVueコンポーネントの新しい形式で従来のOptions APIとは大きく異なります。
下記は、その違いについてわかりやすく解説されていた記事です。

https://jp.skilled.yashio-corp.com/media/vue/5876/#Options_APIVue

また公式ではこのように説明されています。

Composition API では、インポートした各種 API 関数を使ってコンポーネントのロジックを定義していきます。SFC において、Composition API は通常、<script setup> と組み合わせて使用します。setup という属性を付けることで、Vue にコンパイル時の変形操作を実行してほしいというヒントが伝えられます。これにより、定型的な書式の少ない Composition API を利用することができます。

TypeScriptを使用する場合はscriptタグにlang属性も追加します。

<script setup lang="ts">

下記は同じロジックをOpenAPI/CompositionAPIで書いた場合のサンプルコードです。

OpenAPI
<script>
export default {
  data() {
    return {
      count: 0
    }
  },

  methods: {
    increment() {
      this.count++
    }
  },

  mounted() {
    console.log(`The initial count is ${this.count}.`)
  }
}
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>
CompositionAPI
<script setup>
import { ref, onMounted } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}

onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

Ref

ref()は、基本的に単一の値(基本データ型やオブジェクトなど)をリアクティブな値として定義する際に使用するメソッドです。
import { ref } from 'vue'でimportすることにより使用可能になります。
下記のようなRefComponent.vueファイルを作成しました。

/src/components/RefComponent.vue
<script setup lang="ts">
import { ref } from "vue"; // import

const count = ref(0); // リアクティブデータの定義
</script>

<template>
  <h1>RefComponent</h1>
  {{ count }}
  <button @click="count++">クリック</button>
</template>

こちらのコンポーネントをHomeView.vueでimportしhttp://localhost:5173/を開くと下記のようなレンダリングがされているかと思います🌐
※画面をスッキリさせるためにcreat-vueで生成された不要なコードは削除しています。

Vue2ではdataの中で書いていたデータ定義をVue3では上記のようにref()を使用して書くようですね。
こちらのコードはtsの型推論で書いていますが、型注釈に直すと下記の通りです。

/src/components/RefComponent.vue
<script setup lang="ts">
import { ref } from "vue";

const count = ref<number>(0); // ジェネリクスで型注釈
</script>

<template>
  <h1>RefComponent</h1>
  {{ count }}
  <button @click="count++">クリック</button>
</template>

VScodeでhoverしてみてみると、ジェネリクス以外にRefという専用の型があるみたいですね。

(alias) ref<number>(value: number): Ref<number> (+2 overloads)
import ref

この内容に沿ってRefコ型に書き直したコードが下記です。

/src/components/RefComponent.vue
<script setup lang="ts">
import { ref, type Ref } from "vue"; // Refのimport
	
// const count = ref<number>(0);
const count: Ref<number> = ref(0);
</script>

<template>
  <h1>RefComponent</h1>
  {{ count }}
  <button @click="count++">クリック</button>
</template>

Reactive

reactive()ref()同様、リアクティブな値を定義する際に使用します。
ref()は単一の値(基本データ型やオブジェクトなど)の定義に使用するのに対して、reactive()を主にオブジェクトを定義する際に使用します✍🏻

例)

<script setup lang="ts">
import { reactive } from "vue";

type User = {
  id: number;
  name: string;
};
const user1: User = reactive({
  id: 1,
  name: "鈴木",
});
user1.name = "佐藤";
</script>

<template>
  {{ user1.name }}
</template>

イベント

イベントの型定義は、Eventインターフェースを使用します。

例)

<script setup lang="ts">
const handleChange = (e: Event) => { // 引数の型注釈
  console.log((e.target as HTMLInputElement).value); // 型アサーション
};
</script>

<template>
  <input type="text" @change="handleChange" />
</template>

コード解説

型定義をしていない下記コードではエラーが出ます。

パラメーター 'e' の型は暗黙的に 'any' になります。

<script setup lang="ts">
const handleChange = (e) => { 
  console.log(e.target.value);
};
</script>

<template>
  <input type="text" @change="handleChange" />
</template>

Eventインターフェースを使用し引数の型注釈を行います。

<script setup lang="ts">
const handleChange = (e:Event) => { // 引数の型注釈
  console.log(e.target.value);
};
</script>

<template>
  <input type="text" @change="handleChange" />
</template>

すると今度はconsole.log(e.target.value);でエラーが出ます。

'e.target' は 'null' の可能性があります。

nullの可能性とのことでstrictモードだとエラー表示されます。
型アサーションを使用し解消します。

<script setup lang="ts">
const handleChange = (e: Event) => {
  console.log((e.target as HTMLInputElement).value); // 型アサーション
};
</script>

<template>
  <input type="text" @change="handleChange" />
</template>

Computed

処理内で使用している値が変化するとすぐ反映させる、算出プロパティComputedの型付方法です。
ジェネリクスで、型注釈した場合は下記のようなコードになります。

例)

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

const amount = ref<number>(0);
const subTotal = computed<number>(() => amount.value * 1000);
</script>

<template>
  単価: 1000円<br />
  個数: <input type="number" v-model="amount" /><br />
  小計: {{ subTotal }}
</template>

defineProps

親コンポーネントから子コンポーネントにデータを渡す仕組みであるpropsは、defineProps()を使用します。

例)

子コンポーネント
<script setup lang="ts">
// propsする値の型の定義
type Props = {
  id: number;
  name: string;
};

// defineProps()でジェネリクスで型を指定し返却される値を定数に格納
const props = defineProps<Props>();
</script>

<template>
  id: {{ props.id }}<br />
  name: {{ props.name }}
</template>
親コンポーネント
<script setup lang="ts">
import DefinePropsComponent from "@/components/DefinePropsComponent.vue";
</script>

<template>
  <main>
    <DefinePropsComponent :id="1" name="佐藤" />
  </main>
</template>

PropType

配列の型まで指定するには、PropTypeを使用してpropsに定義しておきます。

例)

子コンポーネント
<script setup lang="ts">
import type { PropType } from "vue";

type Props = {
  id: number;
  name: string;
}[];

const props = defineProps({
  items: Array as PropType<Props>,
});
</script>

<template>
  <ul>
    <li v-for="item in props.items" :key="item.id">
      id: {{ item.id }}<br />
      name: {{ item.name }}
    </li>
  </ul>
</template>
親コンポーネント
<script setup lang="ts">
import PropTypeComponent from "@/components/PropTypeComponent.vue";

const items = [
  {
    id: 1,
    name: "商品1",
  },
  {
    id: 2,
    name: "商品2",
  },
  {
    id: 3,
    name: "商品3",
  },
];
</script>

<template>
  <main>
    <PropTypeComponent :items="items" />
  </main>
</template>

defineEmits

子コンポーネントのイベントを検知し親コンポーネントの処理を発火させる際にはdefineEmitsを使用し型を指定します🚀
下記の例では、子コンポーネントでクリックした際に、それを検知し親コンポーネントの処理でconsoleさせたコードです。

例)

子コンポーネント
<script setup lang="ts">
const emit = defineEmits<{
  (e: "emitClick", message: string): void;
}>();

const childFn = (message: string): void => {
  emit("emitClick", message);
};
</script>

<template>
  <button @click="childFn('子コンポーネントでクリックされました')">クリック</button>
</template>
親コンポーネント
<script setup lang="ts">
import defineEvents from "@/components/defineEventsComponent.vue";
const parentFn = (message: string): void => {
  console.log(message);
};
</script>

<template>
  <main>
    <defineEvents @emit-click="parentFn" />
  </main>
</template>

コード解説

下記のような構文でdefineEmitesを使用します。
defineEmites<{ (e: "任意のカスタムイベント名", 引数: その型) : 返却値の型 }>

子コンポーネント
const emit = defineEmits<{
  (e: "emitClick", message: string): void;
}>();

下記では子コンポーネントのボタンクリック時に実行される関数childFnを定義しています。
処理内ではdefineEmits()の返り値が格納されているemit()を使用しています。
emit("任意のカスタムイベント名", 引数)の形で記述します。

子コンポーネント
const childFn = (message: string): void => {
  emit("emitClick", message);
};

下記で、buttonクリック時にchildFn()が実行されます。

子コンポーネント
<template>
  <button @click="childFn('子コンポーネントでクリックされました')">クリック</button>
</template>

親コンポーネントでは@カスタムイベント名=親コンポーネントの関数によって、子コンポーネントでのイベントと親コンポーネントの処理を紐づけています。
子コンポーネントでカスタムイベント名はキャメルケースで記述していたのに対して、親コンポーネント(templateタグ内)ではケバブケースで記述する点に注意してください。

親コンポーネント
<template>
 <defineEvent @emit-click="parentFn" />
</template>

Discussion