Vue3+TypeScript开发实战:解决日常开发的15个真实难题
🔥 本文来自真实项目经验总结,解决开发中最常遇到的 TypeScript 类型问题和业务难点。
作者:沈大大
更新时间:2024-03-08
一、后端接口数据类型处理
1.1 处理不确定的后端返回类型
typescript
// 后端返回数据类型不确定的情况
interface ApiResponse<T = any> {
code: number
data: T
message: string
}
// 处理后端返回的null变为undefined
type NonNull<T> = T extends null ? undefined : T
// 处理后端返回的数字枚举
type Status = '0' | '1' | '2' // 后端返回字符串
interface UserInfo {
status: Status
age: number | null // 后端可能返回null
}
// 实际应用
const getUserInfo = async (id: string) => {
const res = await request<ApiResponse<UserInfo>>('/api/user')
// 优雅处理null
const age = res.data.age ?? 0
// 类型安全的枚举判断
const isActive = res.data.status === '1'
return res.data
}
1.2 处理后端分页数据
typescript
// 分页接口通用类型
interface PaginationParams {
page: number
pageSize: number
total?: number
}
interface TableData<T> {
list: T[]
pagination: Required<PaginationParams>
}
// 实际应用:用户列表
interface UserItem {
id: number
name: string
role: string
}
const useUserList = () => {
const state = reactive<TableData<UserItem>>({
list: [],
pagination: {
page: 1,
pageSize: 10,
total: 0
}
})
const loadData = async () => {
const { data } = await request<ApiResponse<TableData<UserItem>>>({
url: '/api/users',
params: {
page: state.pagination.page,
pageSize: state.pagination.pageSize
}
})
state.list = data.list
state.pagination.total = data.pagination.total
}
return {
state,
loadData
}
}
二、表单开发类型问题
2.1 动态表单字段类型
typescript
// 支持多种表单项类型
type FieldType = 'input' | 'select' | 'datepicker' | 'upload'
interface FormField<T = any> {
key: keyof T
label: string
type: FieldType
rules?: ValidationRule[]
props?: Record<string, any>
}
// 用户编辑表单示例
interface UserForm {
name: string
age: number
role: string
avatar: string
}
const formConfig: FormField<UserForm>[] = [
{
key: 'name',
label: '姓名',
type: 'input',
rules: [{ required: true, message: '请输入姓名' }]
},
{
key: 'age',
label: '年龄',
type: 'input',
props: { type: 'number' }
},
{
key: 'role',
label: '角色',
type: 'select',
props: {
options: [
{ label: '管理员', value: 'admin' },
{ label: '用户', value: 'user' }
]
}
}
]
// 表单数据类型自动推导
const formData = ref<UserForm>({
name: '',
age: 0,
role: '',
avatar: ''
})
2.2 表单验证类型
typescript
// 自定义验证规则
type CustomValidator<T> = (rule: any, value: T) => Promise<void>
// 手机号验证
const validatePhone: CustomValidator<string> = async (rule, value) => {
if (!/^1[3-9]\d{9}$/.test(value)) {
throw new Error('请输入正确的手机号')
}
}
// 密码确认验证
const validateConfirmPassword = (password: string): CustomValidator<string> => {
return async (rule, value) => {
if (value !== password) {
throw new Error('两次密码输入不一致')
}
}
}
// 实际应用
const registerForm = ref({
phone: '',
password: '',
confirmPassword: ''
})
const rules = {
phone: [
{ required: true, message: '请输入手机号' },
{ validator: validatePhone }
],
confirmPassword: [
{ required: true, message: '请确认密码' },
{ validator: validateConfirmPassword(registerForm.value.password) }
]
}
三、组件通信类型问题
3.1 Props 和 Emits 类型定义
typescript
// 子组件
interface Props {
data: TableData[]
loading?: boolean
pagination?: PaginationParams
}
interface Emits {
(e: 'update:pagination', pagination: PaginationParams): void
(e: 'select', item: TableData): void
}
const MyTable = defineComponent({
props: {
data: {
type: Array as PropType<Props['data']>,
required: true
},
loading: Boolean,
pagination: Object as PropType<Props['pagination']>
},
emits: ['update:pagination', 'select'],
setup(props, { emit }) {
// 类型安全的事件触发
const handleSelect = (item: TableData) => {
emit('select', item)
}
return { handleSelect }
}
})
// 父组件使用
const ParentComponent = defineComponent({
setup() {
const handleSelect = (item: TableData) => {
// item 类型自动推导
console.log(item.id)
}
return () => (
<MyTable
data={[]}
onSelect={handleSelect}
/>
)
}
})
3.2 Provide/Inject 类型定义
typescript
// 定义注入的key和类型
interface ThemeContext {
primary: string
isDark: boolean
toggleTheme: () => void
}
const ThemeSymbol = Symbol() as InjectionKey<ThemeContext>
// 提供者
const ThemeProvider = defineComponent({
setup(_, { slots }) {
const state = reactive({
primary: '#1890ff',
isDark: false
})
const toggleTheme = () => {
state.isDark = !state.isDark
}
provide(ThemeSymbol, {
...toRefs(state),
toggleTheme
})
return () => slots.default?.()
}
})
// 使用者
const useTheme = () => {
const theme = inject(ThemeSymbol)
if (!theme) throw new Error('useTheme must be used within ThemeProvider')
return theme
}
四、状态管理类型问题
4.1 Pinia Store 类型定义
typescript
// 用户store
interface UserState {
userInfo: UserInfo | null
permissions: string[]
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
userInfo: null,
permissions: []
}),
getters: {
// 类型自动推导
isAdmin: (state) => state.userInfo?.role === 'admin',
// 手动指定返回类型
permissionCodes(): string[] {
return this.permissions.map(p => p.split(':')[0])
}
},
actions: {
// 异步action类型
async fetchUserInfo() {
const res = await request<ApiResponse<UserInfo>>('/api/user/info')
this.userInfo = res.data
},
// 同步action类型
clearUserInfo() {
this.userInfo = null
this.permissions = []
}
}
})
// 组件中使用
const userStore = useUserStore()
// 类型自动推导
const { userInfo, isAdmin } = storeToRefs(userStore)
五、实用工具类型
typescript
// 移除对象中的可选属性
type RequiredKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K
}[keyof T]
type RequiredProps<T> = Pick<T, RequiredKeys<T>>
// 提取Promise返回值类型
type UnboxPromise<T> = T extends Promise<infer R> ? R : T
// 提取组件Props类型
type ComponentProps<T> = T extends new () => { $props: infer P } ? P : never
// 递归Partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// 实际应用
interface Config {
api: {
baseURL: string
timeout: number
headers?: Record<string, string>
}
theme: {
primary: string
layout?: 'horizontal' | 'vertical'
}
}
// 部分配置更新
const updateConfig = (config: DeepPartial<Config>) => {
// ...
}
六、常见开发难点
6.1 处理第三方库类型
typescript
// 扩展第三方库类型
declare module 'vue-router' {
interface RouteMeta {
title: string
icon?: string
permissions?: string[]
}
}
// 扩展Window对象
declare global {
interface Window {
__INITIAL_STATE__: any
wx: WechatJSSDK
}
}
6.2 处理动态类型
typescript
// 动态表单项类型
type DynamicField<T extends string> = T extends 'input'
? { type: 'input', maxLength?: number }
: T extends 'select'
? { type: 'select', options: Array<{ label: string, value: any }> }
: never
// 动态组件Props类型
type DynamicProps<T extends Record<string, any>> = {
[K in keyof T]: {
type: PropType<T[K]>
required?: boolean
default?: T[K]
}
}
七、开发技巧总结
- 善用类型推导,减少手动类型标注
- 合理使用泛型,提高代码复用性
- 使用类型断言要谨慎,优先使用类型收窄
- 充分利用IDE的类型提示功能
- 保持类型定义的一致性和集中管理
八、推荐工具和配置
- TypeScript 版本 >= 4.5
- VSCode + Volar
- ESLint + @typescript-eslint
- tsconfig.json 关键配置:
- strict: true
- noImplicitAny: true
- strictNullChecks: true
参考资料
💡 开发中遇到 TypeScript 问题?欢迎评论区交流!
🔥 点赞收藏,持续更新 Vue3+TS 实战经验
#Vue3 #TypeScript #前端开发 #实战经验