Skip to main content
Advertisement

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>
<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>
Advertisement