12.1 <script setup lang="ts"> — defineProps and defineEmits Types
Vue 3 + TypeScript Overview
Vue 3's Composition API and <script setup> syntax integrate very well with TypeScript. Specifying lang="ts" gives you complete type safety.
<script setup lang="ts">
import { ref, computed } from 'vue'
// Types are automatically inferred
const count = ref(0) // Ref<number>
const doubled = computed(() => count.value * 2) // ComputedRef<number>
</script>
<template>
<button @click="count++">{{ count }}</button>
<p>{{ doubled }}</p>
</template>
defineProps Type Declarations
Runtime Declaration
<script setup lang="ts">
// Runtime props declaration (Vue actually validates this)
const props = defineProps({
name: {
type: String,
required: true,
},
age: {
type: Number,
default: 0,
},
active: Boolean,
})
</script>
Type-Based Declaration (Recommended)
<script setup lang="ts">
// Define Props using only types — more concise and TypeScript-friendly
interface Props {
name: string
age?: number
active?: boolean
tags?: string[]
}
const props = defineProps<Props>()
// props.name: string
// props.age: number | undefined
</script>
Setting Default Values with withDefaults
<script setup lang="ts">
interface ButtonProps {
label: string
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
}
const props = withDefaults(defineProps<ButtonProps>(), {
variant: 'primary',
size: 'md',
disabled: false,
loading: false,
})
// props.variant: 'primary' | 'secondary' | 'danger' (default: 'primary')
</script>
<template>
<button
:class="['btn', `btn-${props.variant}`, `btn-${props.size}`]"
:disabled="props.disabled || props.loading"
>
<span v-if="props.loading">Loading...</span>
<span v-else>{{ props.label }}</span>
</button>
</template>
defineEmits Type Declarations
Type-Based Emit Declaration
<script setup lang="ts">
// Define emit event types
const emit = defineEmits<{
click: [event: MouseEvent] // Event argument types
change: [value: string]
update: [id: number, data: User] // Multiple arguments
'update:modelValue': [value: string] // v-model event
close: [] // Event with no arguments
}>()
function handleClick(e: MouseEvent) {
emit('click', e)
}
function handleInput(value: string) {
emit('change', value)
emit('update:modelValue', value) // v-model support
}
</script>
v-model Support
<!-- Custom v-model component -->
<script setup lang="ts">
interface Props {
modelValue: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function onInput(e: Event) {
const target = e.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>
<template>
<input :value="props.modelValue" @input="onInput" />
</template>
<!-- Usage in parent -->
<template>
<CustomInput v-model="username" />
<!-- Equivalent: <CustomInput :modelValue="username" @update:modelValue="username = $event" /> -->
</template>
Multiple v-model
<script setup lang="ts">
interface Props {
firstName: string
lastName: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:firstName': [value: string]
'update:lastName': [value: string]
}>()
</script>
<!-- Usage in parent -->
<!-- <FullName v-model:firstName="first" v-model:lastName="last" /> -->
defineModel (Vue 3.4+)
More concise v-model support.
<script setup lang="ts">
import { defineModel } from 'vue'
// Simplified declaration for v-model
const modelValue = defineModel<string>({ required: true })
// Named v-model
const title = defineModel<string>('title')
const count = defineModel<number>('count', { default: 0 })
</script>
<template>
<input v-model="modelValue" />
<input v-model="title" />
<input type="number" v-model="count" />
</template>
defineExpose — External Component API
<!-- ChildComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
function reset() {
count.value = 0
}
// Explicitly select which APIs to expose externally
defineExpose({
count,
increment,
reset,
})
</script>
<!-- ParentComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
// Extract component instance type
type ChildInstance = InstanceType<typeof ChildComponent>
const childRef = ref<ChildInstance | null>(null)
function handleReset() {
childRef.value?.reset()
}
</script>
<template>
<ChildComponent ref="childRef" />
<button @click="handleReset">Reset</button>
</template>
Pro Tips
1. Reusing Props Types
// types/components.ts
export interface TableColumn<T> {
key: keyof T
label: string
sortable?: boolean
width?: number
}
export interface PaginationProps {
page: number
pageSize: number
total: number
}
<script setup lang="ts">
import type { TableColumn, PaginationProps } from '@/types/components'
interface Props extends PaginationProps {
columns: TableColumn<User>[]
data: User[]
}
defineProps<Props>()
</script>
2. Importing External Types with defineProps
Before Vue 3.3, types imported from external files couldn't be used directly in defineProps. From 3.3+ this is possible.
<script setup lang="ts">
// Vue 3.3+: External types can be used directly
import type { User } from '@/types/user'
defineProps<{
user: User // ✅ External types supported
}>()
</script>