T-CREATOR

Vue.js `<script setup>` マクロ辞典:defineProps/defineEmits/defineExpose/withDefaults

Vue.js `<script setup>` マクロ辞典:defineProps/defineEmits/defineExpose/withDefaults

Vue 3 の Composition API では、<script setup> という記法が導入され、コンポーネントの記述がより簡潔で直感的になりました。しかし、いざ使ってみると「definePropsdefineEmits の使い分けは?」「withDefaults はどこで使う?」「外部から子コンポーネントのメソッドを呼びたいときはどうする?」といった疑問が湧いてきませんか。

本記事では、<script setup> で使える 4 つの主要マクロdefinePropsdefineEmitsdefineExposewithDefaults)を、早見表とともに徹底解説します。初心者の方でもすぐに実践できるよう、豊富なコードサンプルと図解で仕組みを丁寧にお伝えしますね。

マクロ早見表

まずは全体像を把握しましょう。以下の表で、各マクロの役割と使いどころを一覧できます。

#マクロ名役割主な用途戻り値
1defineProps親から受け取る props を定義コンポーネントに外部からデータを渡すprops オブジェクト(読み取り専用)
2defineEmits親へイベントを送信する emit 関数を定義子から親へイベント通知・データ送信emit 関数
3defineExpose子コンポーネントの内部を親に公開親が ref 経由で子のメソッド・プロパティを呼び出すなし(公開のみ)
4withDefaultsprops にデフォルト値を設定TypeScript 型定義 + デフォルト値を簡潔に記述デフォルト値付き props 型

構文早見表

次に、各マクロの基本的な構文をまとめます。コピー&ペーストしてすぐに試せるよう、必要最小限の形で記載しました。

#マクロ基本構文例
1defineProps(JavaScript)const props = defineProps({ message: String })
2defineProps(TypeScript)const props = defineProps<{ message: string }>()
3defineEmits(JavaScript)const emit = defineEmits(['update'])
4defineEmits(TypeScript)const emit = defineEmits<{ update: [value: number] }>()
5defineExposedefineExpose({ count, increment })
6withDefaultswithDefaults(defineProps<Props>(), { count: 0 })

図:マクロの役割とデータフロー

以下の図は、親コンポーネントと子コンポーネント間で、各マクロがどのように連携するかを示します。

mermaidflowchart TB
  parent["親コンポーネント"]
  child["子コンポーネント<br/>(<script setup>)"]

  parent -->|"props 渡し<br/>(defineProps)"| child
  child -->|"emit でイベント送信<br/>(defineEmits)"| parent
  parent -->|"ref 経由で呼び出し"| expose["defineExpose<br/>公開メソッド"]
  expose -.->|"内部公開"| child

  style child fill:#e1f5ff
  style expose fill:#fff4e1

図で理解できる要点

  • props(defineProps):親 → 子へのデータ受け渡し
  • emit(defineEmits):子 → 親へのイベント通知
  • expose(defineExpose):親が子の内部メソッドを直接呼び出せるようにする仕組み

背景

<script setup> とは

Vue 3 では、従来の Options API に加えて Composition API が導入されました。Composition API を使うと、ロジックを関数単位で整理でき、再利用性や可読性が向上します。

しかし、Composition API の初期記法は setup() 関数内にすべてを書く必要があり、やや冗長でした。そこで Vue 3.2 から <script setup> が正式導入され、以下のメリットが得られるようになりました。

  • コードが短く、シンプルreturn 文が不要、トップレベルで宣言した変数・関数がすべてテンプレートで利用可能
  • 型推論が強力:TypeScript との相性が良く、型安全性が高い
  • パフォーマンス向上:コンパイル時に最適化される

マクロとは

<script setup> 内で使える definePropsdefineEmits などは「マクロ」 と呼ばれます。マクロはコンパイル時に特別な処理を行う関数で、インポート不要で使えます。

以下の図は、<script setup> が従来の記法と比べてどれだけ簡潔になるかを示しています。

mermaidflowchart LR
  old["Options API<br/>setup() 関数"]
  new["<script setup><br/>マクロ記法"]

  old -->|"冗長な return 文"| result1["コード量:多"]
  new -->|"return 不要"| result2["コード量:少<br/>型推論:強"]

  style new fill:#d4edda
  style result2 fill:#d4edda

図で理解できる要点

  • <script setup> を使うと、return 文が不要になり、コードが大幅に短くなります
  • TypeScript の型推論が効きやすく、開発体験が向上します

課題

<script setup> は便利ですが、従来の Options API とは異なる書き方が求められます。特に以下の点で混乱しがちです。

1. props の受け取り方が不明瞭

Options API では props オプションに定義すれば済みましたが、<script setup> では defineProps マクロを使う必要があります。

2. イベント発火の方法が変わる

従来は this.$emit() を使いましたが、<script setup> では this が存在しません。代わりに defineEmits マクロで emit 関数を取得します。

3. 子コンポーネントの内部にアクセスできない

親が子コンポーネントの ref を取得しても、デフォルトでは内部のメソッドやプロパティにアクセスできません。defineExpose マクロで明示的に公開する必要があります。

4. デフォルト値の設定が煩雑

TypeScript で型定義と props のデフォルト値を同時に設定しようとすると、記述が冗長になりがちです。withDefaults マクロを使うと、型安全性を保ちながら簡潔に書けます。

以下の図は、これらの課題を整理したものです。

mermaidflowchart TD
  start["<script setup> を使う"]
  q1["props を受け取りたい"]
  q2["イベントを発火したい"]
  q3["子の内部を公開したい"]
  q4["デフォルト値を設定したい"]

  start --> q1
  start --> q2
  start --> q3
  start --> q4

  q1 -.->|"解決策"| sol1["defineProps"]
  q2 -.->|"解決策"| sol2["defineEmits"]
  q3 -.->|"解決策"| sol3["defineExpose"]
  q4 -.->|"解決策"| sol4["withDefaults"]

  style start fill:#fff4e1
  style sol1 fill:#d4edda
  style sol2 fill:#d4edda
  style sol3 fill:#d4edda
  style sol4 fill:#d4edda

解決策

それぞれのマクロを使うことで、上記の課題をスマートに解決できます。以下、各マクロの使い方を順に見ていきましょう。

defineProps:親から props を受け取る

基本的な使い方(JavaScript)

defineProps は、親コンポーネントから渡されるデータ(props)を定義するマクロです。

javascript<script setup>
// props を定義(JavaScript)
const props = defineProps({
  message: String,
  count: Number
})

// props を参照
console.log(props.message) // 親から渡された message
</script>

ポイント

  • オブジェクト形式で props 名と型(StringNumber など)を指定します
  • props読み取り専用 です。直接変更するとエラーになります

TypeScript での使い方

TypeScript を使う場合、ジェネリック型で props の型を定義できます。

typescript<script setup lang="ts">
// props を型定義(TypeScript)
const props = defineProps<{
  message: string
  count: number
}>()

console.log(props.message)
</script>

ポイント

  • <{ ... }>() の形で型を渡します
  • JavaScript と異なり、型推論が効くため、エディタの補完が強力になります

バリデーション付きの定義

props に詳細なバリデーションを設定することもできます。

javascript<script setup>
const props = defineProps({
  // 必須 props
  title: {
    type: String,
    required: true
  },
  // デフォルト値付き
  count: {
    type: Number,
    default: 0
  },
  // カスタムバリデーション
  status: {
    type: String,
    validator: (value) => {
      return ['active', 'inactive'].includes(value)
    }
  }
})
</script>

ポイント

  • required: true で必須化
  • default でデフォルト値を設定
  • validator 関数でカスタム検証ルールを追加

defineEmits:親へイベントを送信する

基本的な使い方(JavaScript)

defineEmits は、子コンポーネントから親へイベントを送信する emit 関数を取得するマクロです。

javascript<script setup>
  // イベントを定義(JavaScript) const emit =
  defineEmits(['update', 'delete']) // イベントを発火 const
  handleClick = () =>{' '}
  {emit('update', { id: 1, name: 'Vue' })}
</script>
html<template>
  <button @click="handleClick">更新</button>
</template>

ポイント

  • 配列で発火可能なイベント名を列挙します
  • emit('イベント名', ペイロード) でイベントを発火し、親に通知します

TypeScript での使い方

TypeScript では、イベント名とペイロードの型を厳密に定義できます。

typescript<script setup lang="ts">
// イベントを型定義(TypeScript)
const emit = defineEmits<{
  update: [payload: { id: number; name: string }]
  delete: [id: number]
}>()

const handleUpdate = () => {
  emit('update', { id: 1, name: 'Vue' })
}
</script>

ポイント

  • イベント名とペイロードの型を { イベント名: [引数型] } 形式で定義します
  • 型安全性が高まり、誤ったペイロードの送信を防げます

親コンポーネントでの受け取り方

親側では、@イベント名 でイベントをリッスンします。

html<template>
  <ChildComponent
    @update="handleUpdate"
    @delete="handleDelete"
  />
</template>
javascript<script setup>
  const handleUpdate = (payload) =>{' '}
  {console.log('更新:', payload)}
  const handleDelete = (id) => {console.log('削除:', id)}
</script>

ポイント

  • @イベント名="ハンドラ関数" で子からのイベントを受け取ります
  • ハンドラ関数の引数として、子から送られたペイロードが渡されます

defineExpose:子の内部を親に公開する

基本的な使い方

デフォルトでは、<script setup> 内の変数や関数は外部から参照できません。親が子の内部にアクセスしたい場合、defineExpose マクロで明示的に公開します。

子コンポーネント(Child.vue)

javascript<script setup>
  import {ref} from 'vue' const count = ref(0) const
  increment = () => {count.value++}
  // 親に公開するプロパティ・メソッドを指定 defineExpose({
    (count, increment)
  })
</script>
html<template>
  <div>カウント: {{ count }}</div>
</template>

ポイント

  • defineExpose({ ... }) で、公開したいプロパティやメソッドをオブジェクトで渡します
  • 公開されていないものは、親からアクセスできません

親コンポーネントでの利用

親側では、ref を使って子コンポーネントのインスタンスを取得し、公開されたメソッドを呼び出します。

html<template>
  <Child ref="childRef" />
  <button @click="callChild">子の increment を呼ぶ</button>
</template>
javascript<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

const callChild = () => {
  // 子の公開メソッドを呼び出し
  childRef.value.increment()
  console.log('子の count:', childRef.value.count)
}
</script>

ポイント

  • ref="childRef" で子コンポーネントの参照を取得します
  • childRef.value.メソッド名() で、子の公開メソッドを呼び出せます

使いどころ

defineExpose は以下のシーンで活躍します。

  • フォームコンポーネントの validate() メソッドを親から呼ぶ
  • モーダルコンポーネントの open() / close() を親から制御
  • スクロール位置をリセットする scrollToTop() を外部から実行

ただし、多用すると親子間の結合度が高まるため、本当に必要な場合のみ使いましょう。

withDefaults:デフォルト値を簡潔に設定する

背景:TypeScript での課題

TypeScript で defineProps を使う場合、型定義とデフォルト値を同時に設定しようとすると、記述が冗長になります。

typescript<script setup lang="ts">
// 型定義のみ
const props = defineProps<{
  message: string
  count: number
}>()

// デフォルト値を設定できない...
</script>

この問題を解決するのが withDefaults マクロです。

基本的な使い方

withDefaults を使うと、型定義とデフォルト値を同時に設定できます。

typescript<script setup lang="ts">
// 型定義
interface Props {
  message: string
  count?: number  // オプショナル
  items?: string[]
}

// withDefaults でデフォルト値を設定
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => []  // 配列・オブジェクトは関数で返す
})
</script>

ポイント

  • withDefaults(defineProps<型>(), { デフォルト値 }) の形で記述します
  • オプショナルな props には ? を付けます
  • 配列やオブジェクトのデフォルト値は、ファクトリ関数() => [])で返します

なぜ配列・オブジェクトは関数で返すのか

Vue の props は、複数のコンポーネントインスタンスで共有されます。もしデフォルト値を直接 []{} にすると、すべてのインスタンスが 同じ参照 を持つため、意図しない副作用が起きます。

typescript// ❌ 悪い例:すべてのインスタンスが同じ配列を共有
const props = withDefaults(defineProps<Props>(), {
  items: [], // 危険!
});
typescript// ✅ 良い例:インスタンスごとに新しい配列を生成
const props = withDefaults(defineProps<Props>(), {
  items: () => [], // 安全
});

ポイント

  • プリミティブ型(numberstring など)は直接値を渡せます
  • 参照型(配列、オブジェクト)は必ず関数で返しましょう

実践例:オプション付きボタンコンポーネント

以下は、withDefaults を使った実践的な例です。

typescript<script setup lang="ts">
interface ButtonProps {
  label: string
  type?: 'primary' | 'secondary' | 'danger'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
}

const props = withDefaults(defineProps<ButtonProps>(), {
  type: 'primary',
  size: 'medium',
  disabled: false
})

const emit = defineEmits<{
  click: []
}>()

const handleClick = () => {
  if (!props.disabled) {
    emit('click')
  }
}
</script>
html<template>
  <button
    :class="[`btn-${props.type}`, `btn-${props.size}`]"
    :disabled="props.disabled"
    @click="handleClick"
  >
    {{ props.label }}
  </button>
</template>

ポイント

  • 必須 props(label)とオプショナル props(typesize など)を明確に区別
  • デフォルト値を設定することで、呼び出し側の記述がシンプルになります

具体例

ここまでの内容を踏まえて、実践的なコンポーネントを作成してみましょう。

シナリオ:カウンターコンポーネント

以下の仕様でカウンターコンポーネントを実装します。

  • 親から初期値(initialCount)を受け取る
  • カウントが変更されたら、親にイベントを通知
  • 親から「リセット」メソッドを呼べるようにする
  • デフォルト値は 0

以下の図は、親子間のデータフローを示します。

mermaidsequenceDiagram
  participant Parent as 親コンポーネント
  participant Counter as Counter.vue

  Parent->>Counter: props: initialCount
  Counter->>Counter: カウント操作
  Counter->>Parent: emit('change', newCount)
  Parent->>Counter: childRef.value.reset()
  Counter->>Counter: カウントを 0 にリセット

図で理解できる要点

  • 親 → 子:props で初期値を渡す
  • 子 → 親:emit でカウント変更を通知
  • 親 → 子:ref 経由で reset メソッドを呼び出す

子コンポーネント(Counter.vue)

まず、子コンポーネントを実装します。

型定義とデフォルト値の設定

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

// Props の型定義
interface CounterProps {
  initialCount?: number
}

// デフォルト値付きで props を定義
const props = withDefaults(defineProps<CounterProps>(), {
  initialCount: 0
})
</script>

イベント定義とリアクティブな状態

typescript// イベントを定義
const emit = defineEmits<{
  change: [count: number];
}>();

// リアクティブなカウント
const count = ref(props.initialCount);

カウント操作の実装

typescript// カウントを増やす
const increment = () => {
  count.value++;
  emit('change', count.value);
};

// カウントを減らす
const decrement = () => {
  count.value--;
  emit('change', count.value);
};

// リセット(親から呼ばれる)
const reset = () => {
  count.value = props.initialCount;
  emit('change', count.value);
};

外部公開

typescript// 親に公開
defineExpose({
  reset,
  count  // 読み取り専用として公開
})
</script>

テンプレート

html<template>
  <div class="counter">
    <h3>カウント: {{ count }}</h3>
    <button @click="decrement">-</button>
    <button @click="increment">+</button>
  </div>
</template>
css<style scoped>
.counter {
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 8px;
}

button {
  margin: 0 5px;
  padding: 8px 16px;
  cursor: pointer;
}
</style>

親コンポーネント(Parent.vue)

次に、親コンポーネントで Counter を利用します。

子コンポーネントのインポートと ref の準備

typescript<script setup lang="ts">
import { ref } from 'vue'
import Counter from './Counter.vue'

// 子コンポーネントの参照
const counterRef = ref<InstanceType<typeof Counter> | null>(null)

イベントハンドラの実装

typescript// カウント変更イベントを受け取る
const handleCountChange = (newCount: number) => {
  console.log('カウントが変更されました:', newCount)
}

// 子のリセットメソッドを呼ぶ
const resetCounter = () => {
  counterRef.value?.reset()
}
</script>

テンプレート

html<template>
  <div class="parent">
    <h2>親コンポーネント</h2>

    <!-- 子コンポーネント -->
    <Counter
      ref="counterRef"
      :initial-count="10"
      @change="handleCountChange"
    />

    <!-- 親から子のメソッドを呼ぶボタン -->
    <button @click="resetCounter">
      カウンターをリセット
    </button>
  </div>
</template>
css<style scoped>
.parent {
  padding: 20px;
  background: #f5f5f5;
}

button {
  margin-top: 10px;
  padding: 10px 20px;
}
</style>

動作確認のポイント

このコンポーネントを実行すると、以下の動作を確認できます。

  1. 初期値の反映:Counter は初期値 10 でスタート
  2. イベント通知:+ / - ボタンを押すたびに、親のコンソールに変更が表示される
  3. メソッド呼び出し:親の「カウンターをリセット」ボタンを押すと、Counter が初期値に戻る

まとめ

本記事では、Vue 3 の <script setup> で使える 4 つの主要マクロを徹底解説しました。

マクロのおさらい

マクロ役割使いどころ
defineProps親から props を受け取るコンポーネントに外部からデータを渡す
defineEmits親へイベントを送信子から親へ状態変化を通知
defineExpose子の内部を親に公開親が子のメソッドを直接呼ぶ
withDefaultsデフォルト値を設定TypeScript で型安全にデフォルト値を定義

ベストプラクティス

  • props は読み取り専用:props を直接変更せず、内部で refcomputed を使う
  • emit は明示的に定義:TypeScript でイベント名とペイロードの型を定義すると安全
  • defineExpose は最小限に:親子間の結合度を下げるため、本当に必要な場合のみ使う
  • withDefaults で型安全:配列・オブジェクトのデフォルト値は必ず関数で返す

次のステップ

これらのマクロを習得すると、Vue 3 のコンポーネント設計がぐっと楽になります。ぜひ実際のプロジェクトで試して、Composition API の強力さを体感してください。

さらに深く学びたい方は、以下の公式ドキュメントもご覧くださいね。

関連リンク