Vue.js `<script setup>` マクロ辞典:defineProps/defineEmits/defineExpose/withDefaults
Vue 3 の Composition API では、<script setup> という記法が導入され、コンポーネントの記述がより簡潔で直感的になりました。しかし、いざ使ってみると「defineProps と defineEmits の使い分けは?」「withDefaults はどこで使う?」「外部から子コンポーネントのメソッドを呼びたいときはどうする?」といった疑問が湧いてきませんか。
本記事では、<script setup> で使える 4 つの主要マクロ(defineProps、defineEmits、defineExpose、withDefaults)を、早見表とともに徹底解説します。初心者の方でもすぐに実践できるよう、豊富なコードサンプルと図解で仕組みを丁寧にお伝えしますね。
マクロ早見表
まずは全体像を把握しましょう。以下の表で、各マクロの役割と使いどころを一覧できます。
| # | マクロ名 | 役割 | 主な用途 | 戻り値 | 
|---|---|---|---|---|
| 1 | defineProps | 親から受け取る props を定義 | コンポーネントに外部からデータを渡す | props オブジェクト(読み取り専用) | 
| 2 | defineEmits | 親へイベントを送信する emit 関数を定義 | 子から親へイベント通知・データ送信 | emit 関数 | 
| 3 | defineExpose | 子コンポーネントの内部を親に公開 | 親が ref 経由で子のメソッド・プロパティを呼び出す | なし(公開のみ) | 
| 4 | withDefaults | props にデフォルト値を設定 | TypeScript 型定義 + デフォルト値を簡潔に記述 | デフォルト値付き props 型 | 
構文早見表
次に、各マクロの基本的な構文をまとめます。コピー&ペーストしてすぐに試せるよう、必要最小限の形で記載しました。
| # | マクロ | 基本構文例 | 
|---|---|---|
| 1 | defineProps(JavaScript) | const props = defineProps({ message: String }) | 
| 2 | defineProps(TypeScript) | const props = defineProps<{ message: string }>() | 
| 3 | defineEmits(JavaScript) | const emit = defineEmits(['update']) | 
| 4 | defineEmits(TypeScript) | const emit = defineEmits<{ update: [value: number] }>() | 
| 5 | defineExpose | defineExpose({ count, increment }) | 
| 6 | withDefaults | withDefaults(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> 内で使える defineProps や defineEmits などは「マクロ」 と呼ばれます。マクロはコンパイル時に特別な処理を行う関数で、インポート不要で使えます。
以下の図は、<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 名と型(
String、Numberなど)を指定します 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: () => [], // 安全
});
ポイント:
- プリミティブ型(
number、stringなど)は直接値を渡せます - 参照型(配列、オブジェクト)は必ず関数で返しましょう
 
実践例:オプション付きボタンコンポーネント
以下は、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(type、sizeなど)を明確に区別 - デフォルト値を設定することで、呼び出し側の記述がシンプルになります
 
具体例
ここまでの内容を踏まえて、実践的なコンポーネントを作成してみましょう。
シナリオ:カウンターコンポーネント
以下の仕様でカウンターコンポーネントを実装します。
- 親から初期値(
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>
動作確認のポイント
このコンポーネントを実行すると、以下の動作を確認できます。
- 初期値の反映:Counter は初期値 10 でスタート
 - イベント通知:+ / - ボタンを押すたびに、親のコンソールに変更が表示される
 - メソッド呼び出し:親の「カウンターをリセット」ボタンを押すと、Counter が初期値に戻る
 
まとめ
本記事では、Vue 3 の <script setup> で使える 4 つの主要マクロを徹底解説しました。
マクロのおさらい
| マクロ | 役割 | 使いどころ | 
|---|---|---|
defineProps | 親から props を受け取る | コンポーネントに外部からデータを渡す | 
defineEmits | 親へイベントを送信 | 子から親へ状態変化を通知 | 
defineExpose | 子の内部を親に公開 | 親が子のメソッドを直接呼ぶ | 
withDefaults | デフォルト値を設定 | TypeScript で型安全にデフォルト値を定義 | 
ベストプラクティス
- props は読み取り専用:props を直接変更せず、内部で 
refやcomputedを使う - emit は明示的に定義:TypeScript でイベント名とペイロードの型を定義すると安全
 - defineExpose は最小限に:親子間の結合度を下げるため、本当に必要な場合のみ使う
 - withDefaults で型安全:配列・オブジェクトのデフォルト値は必ず関数で返す
 
次のステップ
これらのマクロを習得すると、Vue 3 のコンポーネント設計がぐっと楽になります。ぜひ実際のプロジェクトで試して、Composition API の強力さを体感してください。
さらに深く学びたい方は、以下の公式ドキュメントもご覧くださいね。
関連リンク
articleVue.js でメモリリーク?watch/effect/イベント登録の落とし穴と検知法
articleVue.js リアクティビティ内部解剖:Proxy/ref/computed を図で読み解く
articleVue.js 本番運用チェックリスト:CSP/SRI/Cache-Control/エラーログの要点
articleVue.js コンポーネント API 設計:props/emit/slot を最小 API でまとめる
articleVue.js `<script setup>` マクロ辞典:defineProps/defineEmits/defineExpose/withDefaults
articleVue.js を macOS + yarn で最短セットアップ:ESLint/Prettier/TS/パスエイリアス
articleWebSocket が「200 OK で Upgrade されない」原因と対処:プロキシ・ヘッダー・TLS の落とし穴
articleWebRTC 本番運用の SLO 設計:接続成功率・初画出し時間・通話継続率の基準値
articleAstro のレンダリング戦略を一望:MPA× 部分ハイドレーションの強みを図解解説
articleWebLLM が読み込めない時の原因と解決策:CORS・MIME・パス問題を総点検
articleVitest ESM/CJS 混在で `Cannot use import statement outside a module` が出る技術対処集
articleテスト環境比較:Vitest vs Jest vs Playwright CT ― Vite プロジェクトの最適解
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来