frontendBaby

公開日

- 13 分で読めます

第11話 「型推論の不思議な力」


どうしてもVue.jsが分からないので森の博士の研究所に押しかけてみたら、人生が激変した子タヌキの話

この物語は、フロントエンド技術を楽しく学ぶことを目的に、生成AIを活用して執筆されています。 技術的な情報の正確性には細心の注意を払っていますが、その内容がすべて真実であることを保証するものではありません。 あくまで学習の補助ツールとして、肩の力を抜いてお楽しみください。


登場人物紹介

  • フロントエンド博士: 森の奥の研究所に住む、フロントエンドのことなら何でも知っている物知り博士。ポン吉の素朴な疑問にいつも優しく(そして面白おかしく)答えてくれる。
  • ポン吉: 好奇心旺盛な子タヌキ。将来の夢はフロントエンドエンジニア。最近Vue.jsを学び始めたが、その奥深さに興味津々。新しい知識をゲットすると、思わず「ポン!」と飛び跳ねる特技あり。

第11話🦝「型推論の不思議な力」

setup関数とreturn。Composition APIの強力さを理解しつつも、その少しの手間が気になっていたポン吉。今日は、博士が予告した「さらなる魔法」を教わるためにやってきた。

ポン吉: 「博士!もっとシンプルに書ける魔法があるって本当ですか?」

博士: 「うむ、本当じゃよ、ポン吉。その魔法の名は<script setup>。多くのVue開発者が愛用しておる、とても便利な呪文じゃ。」

博士は、前回書いたカウンターコンポーネントを、<script setup>を使って書き換えてみせた。

// <script setup> を使った場合
<script setup lang="ts">
import { ref, computed } from 'vue'

// 1. リアクティブな状態の定義
const count = ref(0)

// 2. 状態を更新するメソッドの定義
function increment() {
  count.value++
}

// 3. 状態から派生する値の定義
const doubleCount = computed(() => count.value * 2)

// これだけ! defineComponentもsetupもreturnも不要!
</script>

<template>
  <p>カウント: {{ count }}</p>
  <p>ダブルカウント: {{ doubleCount }}</p>
  <button @click="increment">増やす</button>
</template>

ポン吉: 「ええええっ!?すごい!defineComponentsetup関数も、最後のreturnも全部なくなってる!なのに、テンプレートではcountincrementがちゃんと使えてる…!これって、どういう仕組みなんですか!?」

ポン吉は驚きのあまり、尻尾がポンと膨らんだ。

博士: 「はっはっは。驚いたじゃろう。これこそが、VueのコンパイラとTypeScriptが連携して起こす『型推論』の不思議な力なんじゃよ。」

ポン吉: 「型推論…?」

博士: 「うむ。実際の仕組みはこうじゃ。まず、Vueコンパイラがテンプレートとスクリプト部分の両方を解析する。そして、テンプレートから参照される変数を特定して、それらを含めた仮想的なsetup関数のコードを生成するんじゃ。その生成されたコードを、TypeScriptが解析して型推論を行っておる。つまり、TypeScriptが直接テンプレートを理解しているわけではなく、Vueコンパイラが作った『橋渡し』のコードを基に、賢く型を推論してくれるんじゃよ。」

博士は黒板に、コンパイラとTypeScriptの連携を図解した。

<script setup>
  const count = ref(0)
  function increment() { ... }
</script>

<template>
  {{ count }} <!-- テンプレートでcountを参照 -->
</template>



1. Vueコンパイラがテンプレートを解析
   「あ、countが使われているな」

2. 仮想的なsetup()コードを生成
setup() {
  const count = ref(0)
  function increment() { ... }
  
  return { count, increment } // テンプレートで使用されるもの
}

3. TypeScriptが生成されたコードで型推論を実行

ポン吉: 「なるほど!Vueコンパイラが僕たちの代わりにreturnしてくれて、TypeScriptはその結果を見て型を理解してくれるんですね!」

博士: 「その通りじゃ。ただし、重要な点がある。この自動公開は、<script setup>のトップレベルで定義されたもののみが対象じゃ。関数の中やブロックスコープ内で定義した変数は、テンプレートには公開されないぞ。」

博士: 「その通りじゃ。そして、TypeScriptもそのことを理解しておるから、我々が明示的に型を書かなくても、countRef<number>であることや、incrementが関数であることを正確に把握してくれる。これが『型推論』じゃ。」

博士は、さらに例を示した。

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

// ✅ トップレベル → テンプレートで使える
const globalCount = ref(0)

function someFunction() {
  // ❌ 関数内部 → テンプレートでは使えない
  const localCount = ref(10)
  
  if (true) {
    // ❌ ブロックスコープ内 → テンプレートでは使えない
    const blockCount = ref(20)
  }
}

// ✅ トップレベル → テンプレートで使える
const anotherGlobalVar = 'hello'
</script>

<template>
  <!-- ✅ これは動く -->
  <p>{{ globalCount }}</p>
  <p>{{ anotherGlobalVar }}</p>
  
  <!-- ❌ これらはエラーになる -->
  <!-- <p>{{ localCount }}</p> -->
  <!-- <p>{{ blockCount }}</p> -->
</template>

ポン吉: 「なるほど!トップレベルでないものは見えないんですね。これで、なんでもかんでも自動で公開されるわけじゃないことがわかりました!」

博士: 「その通りじゃ。おかげで、我々はロジックを書くことだけに集中できるし、予期しない変数がテンプレートに漏れることも防げるんじゃよ。」

ポン吉: 「すごい…!defineComponentを使っていたのは、TypeScriptに『これはコンポーネントですよ』と教えるためでもあったけど、<script setup>なら、それすらもVueが良きに計らってくれるんですね!」

博士: 「まさにその通りじゃ、ポン吉。<script setup>は、Composition APIを最もシンプルかつ効率的に書くための、洗練された仕組みなんじゃよ。」

ポン吉は、型を明示的に書かなくても、裏側で賢く型が動いているTypeScriptの世界に、すっかり魅了されていた。

博士: 「さて、コンポーネントの基本的な作り方は、これでほぼマスターしたと言っても良いじゃろう。次回は、コンポーネント同士のもう一つのコミュニケーション方法、『Emits』と、レイアウトを柔軟にする『Slots』の世界を探検してみようかの。」

ポン吉: 「はい!コンポーネントを使いこなせるようになりたいです!ポン!」


🌟 今日のまとめ

  • <script setup> を使うと、defineComponent, setup, returnが不要になり、コードが非常にシンプルになる。
  • <script setup>内のトップレベルで宣言された変数のみが、自動的にテンプレートに公開される。
  • Vueコンパイラがテンプレートとスクリプトを解析し、仮想的なsetup関数を生成。その後、TypeScriptが生成されたコードを基に 型推論 を実行する。
  • 関数内部やブロックスコープ内の変数はテンプレートに公開されないため、意図しない変数の漏洩を防げる。
  • 型は、書かなくてもちゃんと仕事をしてくれている!

次回予告 「EmitsとSlotsの世界」

子から親へイベントを伝えるEmitsと、コンポーネントの一部を外部から差し込めるようにするSlots。コンポーネント設計をより柔軟にする、これらの強力な機能の型定義に迫ります!

👨‍🏫 博士からの補足

ポン吉は<script setup>の便利さに感動しておったが、実はこの魔法にも「落とし穴」があるんじゃ。便利だからといって、何でもかんでもトップレベルに書いてしまうのは危険じゃぞ。

まず、よくある間違いを見てみよう:

<script setup lang="ts">
// ❌ 悪い例:何でもトップレベルに書いてしまう
const userName = ref('ポン吉')        // ✅ テンプレートで使うから必要
const debugMessage = ref('デバッグ中') // ❌ 内部処理用なのにテンプレートに公開される
const apiUrl = 'https://api.example.com' // ❌ 定数もテンプレートに公開される
const cache = new Map()               // ❌ キャッシュまで公開される
const tempCounter = ref(0)            // ❌ 一時的な計算用なのに公開される
</script>

<template>
  <!-- 実際にはuserNameしか使わないのに... -->
  <p>こんにちは、{{ userName }}さん!</p>
</template>

これの何が問題かというと:

  1. パフォーマンスの無駄:Vueのリアクティブシステムが、使われもしない変数まで監視してしまう
  2. 可読性の悪化:「このコンポーネントは実際に何を外部に提供しているのか」が分からない
  3. 意図しないアクセス:テンプレートから予期しない変数にアクセスできてしまう

解決策は、「必要なもの以外は隠す」ことじゃ:

<script setup lang="ts">
// ✅ 良い例:テンプレートで使うもののみトップレベル
const userName = ref('ポン吉')

// 内部処理は関数内に隠す
function handleApiCall() {
  const apiUrl = 'https://api.example.com' // ここなら漏れない
  const cache = new Map()                  // ここなら漏れない
  
  // 必要に応じて、上のuserNameを更新
  userName.value = '新しい名前'
}

// 複雑な処理も関数でまとめる
function processData() {
  const tempCounter = ref(0)  // 一時的な変数は関数内で
  // ...複雑な処理...
  return result
}
</script>

大規模なプロジェクトでは、時には明示的なsetup関数とreturnを使った方が、チームメンバーにとって分かりやすい場合もあるんじゃよ。

覚えておいてほしいのは:便利な魔法ほど、適切な使い方が重要だということじゃ。<script setup>は素晴らしいツールじゃが、「何でも楽にできる」と「何でもしていい」は別の話なんじゃよ。


第11話 おわり