Vue 3 with Typescript 超入門

# はじめに
まとまった記事がなかったので書く。
ターゲット層は主にJavaやPythonなど、既に他のプログラミング言語を触ったことのある人。

# そもそもVue.jsで何ができる？
 - 機能（コンポーネント）ごとに分けて開発することができる
   - 単一責任の原則
 - たとえば、ブラウザ上に表示される機能だけでも、画面、入力フォーム、表示部分など
   - <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287660/7b57cc30-574a-f4b6-d28c-7b4b5c56ac49.png" width=70% border="0">
 - Vue.jsに限った話ではないけれど...Typescript含め、Javascriptはクライアントサイドで動くHTMLの補助スクリプト的な側面が強かった
 - なので機能ごとに分けて開発する、みたいな概念は他の言語だと当たり前だけど粗雑だった
   - が、ここ最近になって急速に発展した
     - [銀の弾丸ではないTypeScriptを、何故使うべきなのか](https://qiita.com/hariNEzuMI928/items/2b10773a016dff9d0dc0)
 - 要はJavascriptもそろそろ他のプログラミング言語と同じ機能がほしいよね、というお話
 - したがって他のプログラミング言語を触っていればあまり学習コストはかからない（と思う）
 - リアルタイムにデータを反映させることも得意
   - たとえばYouTubeライブの視聴者数とか。
     - これまでだとページ更新するとか、ボタン押してイベント発火させるとか、そういうのが必要だった
<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="ggmdKy" data-pen-title="Simple Vue Example" data-editable="true" data-user="Wyrlor" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/Wyrlor/pen/ggmdKy">
  Simple Vue Example</a> by Joseph Gengarella (<a href="https://codepen.io/Wyrlor">@Wyrlor</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# 天気アプリ
作成した天気アプリになぞって入門解説する。環境構築等は他記事へ。

 - アプリ
   - https://cauuchy.github.io/vue-weather-app/
 - コード
   - https://github.com/cauuchy/vue-weather-app

処理内容としては

 1. コンボボックスで地域を選択
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287660/b37289fd-6a4e-c51a-122c-a13e6c129251.png" width=90% border="0">
 
 2. 選択を検知したら、各地域に応じて天気情報を取得するAPIを叩く
    ```
    https://api.open-meteo.com/v1/forecast/?latitude=${latitude}&longitude=${longitude}&daily=weathercode,temperature_2m_max,precipitation_sum&timezone=Asia/Tokyo
    ```
 3. レスポンスを加工して表示
     <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287660/5e6efc13-0068-450f-96f2-88eb9328b6e0.png" width=90% border="0">


というきわめてシンプルなWebアプリ。

### ディレクトリ構成
 - こんな感じ
    - <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287660/abadb1c7-4e52-5a09-9eae-17beac4404d4.png" border="0">

### Vue.jsの基本構成
```vuejs:App.vue
<template>
  <!-- ここにHTML -->
</template>

<script setup lang="ts">
  // ここにTypescript(Javascript)
</script>

<style>
  /* ここにCSS */
</style>
```

### コンポーネントの表示
#### 親子関係
 - 親コンポーネント、子コンポーネントという概念が存在する
   - <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287660/7b57cc30-574a-f4b6-d28c-7b4b5c56ac49.png" width=70% border="0">
 - 画面が親で、画面(App)に機能(Form, Display)が乗っているイメージ
    ```vuejs:App.vue
    <template>
      <div>
        <Form />
        <Display />
      </div>
    </template>

    <script setup lang="ts">
    import { ref } from "vue";
    import Form from "./components/Form.vue";
    import Display from "./components/Display.vue";
    ...
    ```
    - コンポーネントの`*.vue`ファイルを`import`して、`template`内で`<Form />`のように書く
    - なお、`.ts`から`import`したいデータは`export`する
        ```typescript:weather.ts
        // importするものはexportする
        export interface Weather {
          date: string;
          weatherCode: number;
          temperature: number;
          rainProb: number;
          sunset?: string;
          sunrise?: string;
        }
        ```
### リアクティブ
 - コード内部の値が変わると、画面にも即時反映させる機能
   - つまり、リアルタイムで値が変わる
   - Vue.jsの核の部分
 - `ref`
   - 文字列や数値などのプリミティブな値に使用する（とされている）
   - 実用上はオブジェクト型でも使われている
   - `ref<型>(データ)`
       ```vuejs:App.vue
       const weatherData = ref<WeatherData>(new WeatherData([]));
       ```
   - 値の設定、取得は`.value`でアクセスする
       ```vuejs:App.vue
       weatherData.value = new WeatherData(data)
       console.log("Fetched weather data:", weatherData.value);
       ```
  - `reactive`
    - オブジェクトや配列などのデータ型に使用する
    - `ref`と違ってそのままアクセス可能
    - リアクティブでない変数と区別ができないので、あまり使われないらしい
    - `reactive(オブジェクトなど)`
       ```vuejs
       const state = reactive({
         message: 'Hell World!'
       });

       console.log(state.message);
       ```
### ディレクティブ
- HTMLに対して独自の属性を追加することで、動的な変化を与える役割を果たす
  - マスタッシュ構文
    - `{{ }}` で値を埋め込むことができる
      ```vuejs:Display.vue
      <td>{{ day.date }}</td>
      <td>{{ getWeatherDescription(day.weatherCode) }}</td>
      <td>{{ day.temperature }}</td>
      <td>{{ day.rainProb }}</td>
      ```
  - `v-if`
    - 中身が`true`のときに要素を表示　
       ```vuejs:Display.vue
       <div v-if="weatherValue.getRawWeatherData.length">
       <p>平均気温: {{ props.weatherValue.getAverageTemperature() }}°C</p>
       <p>最大気温: {{ props.weatherValue.getMaxAverageTemperature() }}°C</p>
       </div>
      ```
  - `v-model`
    - input要素やselect要素など、ユーザーが入力した値を同期的に保持する
    - 内容をリアルタイムで更新
       ```vuejs:Form.vue
       <template>
         <div>
           <VueMultiselect placeholder="選択してください" v-model="selectedPrefecture" v-bind:options="prefectures" label="name" />
         </div>
       </template>

       <script setup lang="ts">
       import { ref, defineEmits } from "vue";
       import VueMultiselect from "vue-multiselect";

       const prefectures = [
         { name: "北海道(札幌市)", latitude: 43.0643, longitude: 141.3468 },
         { name: "東京(新宿区)", latitude: 35.6895, longitude: 139.6917 },
         ...
       ]

       const selectedPrefecture = ref<typeof prefectures[0] | null>(null);
      ```
  - `v-bind`
    - HTML要素の属性値とVue側の値を対応（バインド）させる
    - 以下だとコンボボックスの`options`項目を`prefectures`の`name`と対応させている
       ```vuejs:Form.vue
       <template>
         <div>
         <VueMultiselect placeholder="選択してください" v-bind:options="prefectures" label="name" />
         </div>
       </template>
       ...
       const prefectures = [
         { name: "北海道(札幌市)", latitude: 43.0643, longitude: 141.3468 },
         { name: "東京(新宿区)", latitude: 35.6895, longitude: 139.6917 },
         { name: "愛知(名古屋市)", latitude: 35.1802, longitude: 136.9065 },
         { name: "大阪(大阪市)", latitude: 34.6863, longitude: 135.5200 },
         { name: "福岡(福岡市)", latitude: 33.6063, longitude: 130.4179 },
         { name: "謎の都市", latitude: 1000, longitude: 1000 },
       ];
       ...
      ```
    - `v-bind:`は`:`に省略可能
      - ちなみに以下は`for`で繰り返される部分に識別子をバインドしている（でないと区別できない）
      - このように`v-bind`は他のディレクティブと併用することが多い
      ```vuejs:Form.vue
        <tr v-for="day in weatherValue.getRawWeatherData" :key="day.date">
          <td>{{ day.date }}</td>
          <td>{{ getWeatherDescription(day.weatherCode) }}</td>
          <td>{{ day.temperature }}</td>
          <td>{{ day.rainProb }}</td>
        </tr>
      ```
  - `v-on`
    - 要素にイベントリスナーを設定する(select, changeなど)
    - コンボボックスで選択されると`onSelect`が走る
    ```vuejs:Form.vue
    <VueMultiselect placeholder="選択してください" v-on:select="onSelect" v-model="selectedPrefecture" v-bind:options="prefectures" label="name" />
    ...
    function onSelect() {
      if (selectedPrefecture.value) {
      const { latitude, longitude } = selectedPrefecture.value;
      emit("selectLocation", latitude, longitude);
      }
    }
    ```
    - `v-on:`は`@`に省略可能
      - `selectLocation`の変更を検知すると`fetchWeather`が走る
    ```vuejs:App.vue
    <Form @selectLocation="fetchWeather" />
    ```
### Emit
 - 子コンポーネントから親コンポーネントにデータを渡す
   - たとえばフォーム情報を受け取って対応するデータを表示することを考える
   - 子同士間のデータ移動はできない
     - `Form.vue`と`Display.vue`
   - つまり、親に一旦渡してから別の子に渡す
     - `Form.vue` ⇔ `App.vue` ⇔ `Display.vue`
    ```typescript:Form.vue
    // 地域が選択されたらAPI用にlatitudeとlogitudeを親Appに渡す
    function onSelect() {
      if (selectedPrefecture.value) {
       const { latitude, longitude } = selectedPrefecture.value;
       // Emitの設定
       // emit("イベント名", 引数...)
       emit("selectLocation", latitude, longitude);
      }
    }

    // Emitの定義
    // const emit = defineEmits<{(e: "イベント名", 引数): 型,...}();
    const emit = defineEmits<{
      (e: "selectLocation", latitude: number, longitude: number): void;
    }>();
    ```
    ```vuejs:App.vue
    <!-- @イベント名="実行関数"  Emitsのイベント名を指定する -->
    <Form @selectLocation="fetchWeather" />
    ```
    - 上記の場合はフォーム内のコンボボックスで、他の項目が選択された場合に新たな処理をしたいので、`v-on`で渡している
      - たとえば`大阪(大阪市)`を選択すると`App.vue`を経由して天気情報が表示される
    - 子コンポーネントに`defineEmits`でEmitを定義し、`emit()`で設定する
    - 親コンポーネントは`@イベント名="実行関数"`で受け取る
    - 複数のデータを渡したい場合はオブジェクトの形で渡す
        - `defineEmits<{(e: "Event1", value1: type1): void, (e: "Event2"): void, ...}()`
### Props
 - 親コンポーネントから子コンポーネントにデータを渡す
   - `Emit`で説明した `App.vue` ⇔ `Display.vue` の部分
    ```vuejs:App.vue
    <!-- :共有変数="実際のオブジェクト"  Propsで渡すデータを指定する -->
    <Display :weatherValue="weatherData" />
    ```
    ```typescript:Display.vue
    // const props = defineProps<{ 親コンポーネントから受け取るオブジェクト }>();
    const props = defineProps<{ weatherValue: WeatherData }>();
    ```
    - 基本的には`v-bind`で渡し、子コンポーネントは`v-bind`のキー名で受け取る
    - 本来は`props.`でアクセスするが、省略できる
        ```vuejs:Display.vue
        <!-- props.がなくてもアクセスできる -->
        <tbody v-if="weatherValue.getRawWeatherData.length">
          <tr v-for="day in weatherValue.getRawWeatherData" :key="day.date">
        ...
          <!-- props.でアクセスしてもよい -->
          <p>平均気温: {{ props.weatherValue.getAverageTemperature() }}°C</p>
          <p>最大気温: {{ props.weatherValue.getMaxAverageTemperature() }}°C</p>
        ```
    - 複数のデータを渡したい場合はオブジェクトの形で渡す
        - `defineProps<{ value1: type1, value2: type2, ... }>()`

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287660/fddb446a-0d44-7488-ee06-17ae15455087.png" width=70% border="0" style="box-shadow: 0px 0px 0px 0px">

### TypeScript
#### 宣言構文
 - `var`
   - **再代入：可能、再定義：可能**
   - ES6以前に使われていたもので、当時はこれしか使えなかった
   - 再代入も再定義もできるアブナイ爆弾なので、今はほとんど使われない
   ```typescript
   var x = 10;
   x = 20;
   var x = 5;
   ```
 - `let`
   - **再代入：可能、再定義：不可能**
   - 再代入する必要があるものはこれを使う
   ```typescript:weather.ts
    getAverageTemperature(): number | string {
      let total = 0;
      for (const day of this.data) {
        total += day.temperature;
      }
      // let total = 10; ←これはできない（この位置で）
      return (total / this.data.length).toFixed(1);
    }
   ```
 - `const`
   - **再代入：不可能、再定義：不可能**
   - たぶん一番使われている。定数的な扱い
   ```typescript:weatherService.ts
   const API_URL = "https://api.open-meteo.com/v1/forecast";
   // API_URL = "https://example.com" ←これはできない
   // const API_URL = "test" ←これもできない
   ```
#### 型注釈
 - 変数の型を定義できる
   ```typescript:weather.ts
   export interface Weather {
     date: string;
     weatherCode: number;
     temperature: number;
     rainProb: number;
     sunset?: string;
     sunrise?: string;
   }
   ```
 - このためにTypescriptができたといっても過言ではない
 - とはいえJavascriptが他のプログラミング言語に追いついただけなので、あまり真新しさはない
   - 再掲：[銀の弾丸ではないTypeScriptを、何故使うべきなのか](https://qiita.com/hariNEzuMI928/items/2b10773a016dff9d0dc0)
#### 型推論
 - 型注釈を（安全に）省略できる
   ```typescript:weather.ts
   let total = 0;
      for (const day of this.data) {
        total += day.temperature;
   }
   ```
  - 関数の引数は型推論できない
    ```typescript:App.vue
    async function fetchWeather(latitude: number, longitude: number) { ...
    ```
#### プリミティブ型(基本データ型)
 - 他のプログラミング言語とほぼ一緒
   - `number`：数値型
   - `string`：文字型
   - `booolean`：真偽値型
   - `null`
   - `undefined`

など

#### 複合型
- これもユニオン型以外ほぼ一緒
   - 配列
     - `[]`で囲む
     ```typescript
     let numbers: number[] = [1, 2, 3, 4, 5];
     ```
   - オブジェクト
     - `{}`で囲む
     ```typescript:Form.vue
     { name: "北海道(札幌市)", latitude: 43.0643, longitude: 141.3468 },
     ```
   - ユニオン型
     - 変数の型を複数許容するデータ型。`|`でつなげる。
     - たとえば以下のようにしておけば、`toFixed()`で返却されるのは`string`だがこれを削除してもエラーにならない（`number`も受け付ける）
     ```typescript:weather.ts
     getAverageTemperature(): number | string {
      ...
      return (total / this.data.length).toFixed(1);
     }
     ```
#### 基本構文
 - `for`だけちょっと真新しいかも
   - `if`
      ```typescript:Display.vue
      const getWeatherDescription = (code: number): string => {
        if (code >= 0 && code <= 1) return "快晴";
        else if (code <= 3) return "晴れ";
        else if (code <= 45) return "曇り";
        else if (code <= 80) return "雨";
        else if (code <= 95) return "雷雨";
        else return "雪";
      }
     ```
   - `switch`
     ```typescript
     switch (value) {
       case 0:
       case 1:
         return "快晴";
         break;
       case 2:
       case 3:
         return "晴れ";
         break;
       default:
         return "晴れていない";
       break;
     }
     ```
   - `while`
     ```typescript
     let count = 10;
     while (count > 0) {
       count--;
     }
     ```
   - `try-catch`
     ```typescript:App.vue
     try {
       const data = await fetchWeatherData(latitude, longitude);
       weatherData.value = new WeatherData(data);
       console.log("Fetched weather data:", weatherData.value);
     } catch (err) {
       console.error("Weather data fetch failed:", err);
       weatherData.value = new WeatherData([]);
     }
     ```
   - `for`
     ```typescript
     for (let i = 0; i < 10; i++) {
      console.log(i);
     }
     ```
   - `for...in`
     - リストの先頭から末尾まで走査し、**各要素のインデックス**が返される
     ```typescript:weather.ts
     for (const day in this.data) { // dayはthis.dataにおける各要素のインデックス
       maxTemp = Math.max(maxTemp, this.data[day].temperature);
     }
     ```
   - `for...of`
     - リストの先頭から末尾まで走査し、**各要素の値**が返される
     ```typescript:weather.ts
     for (const day of this.data) {  // dayはthis.dataにおける各要素の値
       total += day.temperature;
     }
     ```
#### 関数
  - 普通の関数
    ```typescript
    function onSelect() {
      if (selectedPrefecture.value) {
        const { latitude, longitude } = selectedPrefecture.value;
        emit("selectLocation", latitude, longitude);
      }
    }
    ```
 - アロー関数
   - `(引数) => {処理}`の形で関数を超簡略化
   ```typescript:Display.vue
   const getWeatherDescription = (code: number): string => {
    if (code >= 0 && code <= 1) return "快晴";
    ...
    else return "雪";
   }
   /* 以下と同じ
   const getWeatherDescription = (function(code: number): string {
     if (code >= 0 && code <= 1) return "快晴";
     ...
     else return "雪";
   })
   */
   ```
##### インタフェース
 - 他のプログラミング言語とほぼ一緒
 - ちなみに変数名に続いて **?** をつけると、あってもなくてもいいプロパティになる（オプショナルプロパティ）
   ```typescript:weather.ts
   export interface Weather {
    date: string;
    weatherCode: number;
    temperature: number;
    rainProb: number;
    sunset?: string;
    sunrise?: string;
   }
   ```
##### クラス
 - これもほぼ一緒だが、`setter/getter`用のキーワードが用意されている
 - `setter`は戻り値がないので型定義不要
   ```typescript:weather.ts
   export class WeatherData {
    public data: Weather[];
  
    constructor(data: Weather[]) {
      this.data = data;
    }

    set setWeatherData(data: Weather[]) {
      this.data = data;
    }

    get getRawWeatherData(): Weather[] {
      return this.data;
    }
    ...
   }
   ```
##### 非同期処理
  - `async`で非同期関数を定義し、`await`で非同期処理が完了するのを待つ
    ```typescript:weatherService.ts
    export async function fetchWeatherData(latitude: number, longitude: number): Promise<Weather[]> {
    const response = await fetch(
      `${API_URL}?latitude=${latitude}&longitude=${longitude}&daily=weathercode,temperature_2m_max,precipitation_sum&timezone=Asia/Tokyo`
    );

    const responseCode: boolean = response.ok

    if(responseCode) {
      const data = await response.json();
      ...
    }
    ```
 - 非同期処理には他の書き方も存在するが、今のところ上記の書き方が最先端で簡潔
   - [async/await 入門（JavaScript）](https://qiita.com/soarflat/items/1a9613e023200bbebcb3)

### 今回話さなかったこと
 - Vue 3 には別の書き方もある(Options API)
     - [Vue3デビューcomposition APIとoptions APIについて簡単に比較してみた](https://qiita.com/tsukasaI/items/334beb6d2443df3b42bd)
     - 今回解説したのはComposition API
     - Composition APIのほうが新しくてスッキリ書ける
 - Vue Router
     - ページ遷移処理を簡単にする
     - ページリンクとかをまとめて書ける
     - [はじめに - Vue Router](https://v3.router.vuejs.org/ja/guide/#html)