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

# 初めに

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

**想定読者**

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

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

https://qiita.com/__knm__/items/85a0bc3d248d4d996feb

## 環境構築

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`で環境構築をしていきます。
ちなみに`Vita`も`Vue`も、アジア出身のEvan You（エヴァン・ヨー）という方が開発しています。
天才エンジニアですね🙌

`npm create vue@3`

上記コマンドを実行すると、色々と質問されますので回答していけば雛形の作成が完了します。
![](https://storage.googleapis.com/zenn-user-upload/17388e60fda6-20230619.png)

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

上記コマンドでローカルサーバーが立ち上がるかと思います🎉
秒で終わりますね。
![](https://storage.googleapis.com/zenn-user-upload/6829cf0d3cda-20230619.png)

## 拡張機能インストール

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

### Vue Language Features (Volar)

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

![](https://storage.googleapis.com/zenn-user-upload/1bc78756caf8-20230619.png)

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

### TypeScript Vue Plugin (Volar)

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

![](https://storage.googleapis.com/zenn-user-upload/6a2d771159b0-20230619.png)

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


:::note warn
Volar は Vue 2 の公式 VSCode 拡張機能である Vetur を置き換えるものです。現在 Vetur がインストールされている場合、Vue 3 のプロジェクトで必ず無効化してください。
:::

## フォルダ構成

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

また[公式](https://ja.vuejs.org/guide/introduction.html#api-styles)ではこのように説明されています。

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

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

`<script setup lang="ts">`

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

```vue: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>
```

```vue: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ファイルを作成しました。

```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で生成された不要なコードは削除しています。
![](https://storage.googleapis.com/zenn-user-upload/03966f5e2736-20230620.gif)

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

```vue:/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コ型に書き直したコードが下記です。
	
```vue:/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()`を主にオブジェクトを定義する際に使用します✍🏻

例)

```vue
<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`インターフェースを使用します。

例)
```vue
<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' になります。

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

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

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

```vue
<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モードだとエラー表示されます。
型アサーションを使用し解消します。

```vue
<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の型付方法です。
ジェネリクスで、型注釈した場合は下記のようなコードになります。

例)

```vue
<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()`を使用します。

例)

```vue:子コンポーネント
<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>
```

```vue:親コンポーネント
<script setup lang="ts">
import DefinePropsComponent from "@/components/DefinePropsComponent.vue";
</script>

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

## PropType

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

例)

```vue:子コンポーネント
<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>
```

```vue:親コンポーネント
<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させたコードです。

例)

```vue:子コンポーネント
<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>
```

```vue:親コンポーネント
<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: "任意のカスタムイベント名", 引数: その型) : 返却値の型 }>`

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

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

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


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

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

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

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

また、下記のような書き方もできます。
```vue:子コンポーネント
<script setup lang="ts">
import targetIcon from '@/assets/images/target.svg';

const emit = defineEmits(['emit-test']);

const handleClick = () => {
    emit('emit-test');
};
</script>
<template>
    <button
        @click="handleClick"
    >
        Emit Test
    </button>
</template>
```