为什么组合式API更适合TypeScript
Vue3的选项式API(Options API)配合TypeScript存在天然的类型推导问题——data、computed、methods中的属性需要手动标注类型,this的上下文推断依赖Vue的内部类型体操。组合式API(Composition API)使用普通变量和函数组织逻辑,TypeScript的类型推导开箱即用,不需要额外的类型声明或defineComponent包装。
更重要的是,组合式API的逻辑复用粒度更细。选项式API只能通过Mixins复用,存在命名冲突和来源不透明的硬伤;组合式API通过Composables函数复用逻辑,来源明确、类型安全、可组合嵌套。
Composables函数的TypeScript类型设计
一个规范的Composable函数需要清晰的入参类型、返回值类型和泛型约束:
// composables/useTable.ts
import { ref, reactive, computed, type Ref } from 'vue'
// 分页参数类型
interface Pagination {
page: number
pageSize: number
total: Ref<number>
}
// 表格数据请求函数的约束
interface TableRequest<T> {
(params: Record<string, unknown>): Promise<{ list: T[]; total: number }>
}
// 泛型Composable:支持任意行数据类型
export function useTable<T extends Record<string, unknown>>(
fetchFn: TableRequest<T>,
defaultPageSize = 10
) {
const loading = ref(false)
const dataSource = ref<T[]>([]) as Ref<T[]>
const pagination = reactive<Pagination>({
page: 1,
pageSize: defaultPageSize,
total: ref(0)
})
const fetch = async (extraParams?: Record<string, unknown>) => {
loading.value = true
try {
const { list, total } = await fetchFn({
page: pagination.page,
pageSize: pagination.pageSize,
...extraParams
})
dataSource.value = list
pagination.total.value = total
} finally {
loading.value = false
}
}
const handlePageChange = (page: number) => {
pagination.page = page
fetch()
}
return {
loading,
dataSource,
pagination: computed(() => ({
current: pagination.page,
pageSize: pagination.pageSize,
total: pagination.total.value
})),
fetch,
handlePageChange
}
}
泛型<T>让dataSource的类型根据API返回值自动推导,组件中使用时无需重复声明类型。
业务组件Props的严格类型约束
Vue3.3+支持defineProps的泛型写法,比运行时声明更简洁且类型更严格:
<!-- components/DataTable.vue -->
<script setup lang="ts" generic="T extends Record<string, unknown>">
import type { PropType } from 'vue'
// 列配置类型
interface Column {
key: keyof T
title: string
width?: number
sortable?: boolean
render?: (value: T[keyof T], row: T) => string
}
const props = defineProps<{
data: T[]
columns: Column[]
loading?: boolean
rowKey?: keyof T
selectable?: boolean
}>()
const emit = defineEmits<{
(e: 'rowClick', row: T): void
(e: 'selectionChange', rows: T[]): void
}>()
// 类型安全的行点击处理
const handleRowClick = (row: T) => {
emit('rowClick', row)
}
</script>
泛型组件的关键优势:Column.key的类型是keyof T,只能传入数据对象中存在的字段名,拼写错误在编译期直接报错。
组件事件与Slot的类型安全设计
Slot的Props类型推导是Vue3.3的另一大改进:
<!-- components/DataTable.vue template -->
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="String(col.key)" :style="{ width: col.width + 'px' }">
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="String(row[rowKey ?? 'id'])" @click="handleRowClick(row)">
<td v-for="col in columns" :key="String(col.key)">
<slot :name="String(col.key)" :row="row" :value="row[col.key]">
{{ col.render ? col.render(row[col.key], row) : row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
使用方的Slot可以拿到完整类型:
<!-- 页面中使用DataTable -->
<script setup lang="ts">
interface User {
id: number
name: string
email: string
status: 'active' | 'inactive'
}
// dataSource 类型自动推导为 Ref<User[]>
const { loading, dataSource, fetch } = useTable<User>(fetchUserList)
</script>
<template>
<DataTable :data="dataSource" :columns="columns" :loading="loading">
<!-- name列自定义渲染 -->
<template #name="{ row }">
<!-- row 类型自动推导为 User -->
<a :href="`/users/${row.id}`">{{ row.name }}</a>
</template>
</DataTable>
</template>
组件库的统一导出与Tree-shaking优化
多个可复用组件需要统一的导出入口,方便业务项目按需引入:
// components/index.ts
export { default as DataTable } from './DataTable.vue'
export { default as SearchForm } from './SearchForm.vue'
export { default as ConfirmDialog } from './ConfirmDialog.vue'
export { useTable } from '../composables/useTable'
export { useForm } from '../composables/useForm'
export { usePermission } from '../composables/usePermission'
业务项目中按需引入:
// 自动按需导入(推荐)
// vite.config.ts 配置 unplugin-vue-components
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
Components({
dirs: ['src/components'],
extensions: ['vue'],
deep: true,
dts: 'src/components.d.ts' // 自动生成类型声明文件
})
]
})
这种配置下,组件不需要手动import,模板中直接使用DataTable即可,构建工具自动处理按需引入和类型声明。
组件单元测试的TypeScript类型保障
使用Vitest + Vue Test Utils进行组件测试时,TypeScript类型保障同样重要:
// __tests__/DataTable.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import DataTable from '../DataTable.vue'
import type { ComponentMountingOptions } from '@vue/test-utils'
interface User {
id: number
name: string
email: string
}
const mockData: User[] = [
{ id: 1, name: 'Alice', email: 'alice@test.com' },
{ id: 2, name: 'Bob', email: 'bob@test.com' }
]
const columns = [
{ key: 'name' as const, title: '姓名' },
{ key: 'email' as const, title: '邮箱' }
]
describe('DataTable', () => {
it('渲染正确的行数', () => {
const wrapper = mount(DataTable, {
props: { data: mockData, columns }
})
expect(wrapper.findAll('tbody tr')).toHaveLength(2)
})
it('点击行触发rowClick事件', async () => {
const wrapper = mount(DataTable, {
props: { data: mockData, columns }
})
await wrapper.findAll('tbody tr')[0].trigger('click')
expect(wrapper.emitted('rowClick')![0]).toEqual([mockData[0]])
})
})
类型安全的组件体系,让编译期就能捕获Props传递错误、事件参数不匹配、Slot数据类型不一致等问题。相比运行时报错定位,编译期错误的修复成本几乎为零。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-pei-he-typescript-gou-jian-ke-fu-yong-ye/