Vue3+TypeScript开发实战:解决日常开发的15个真实难题

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]
  }
}

七、开发技巧总结

  1. 善用类型推导,减少手动类型标注
  2. 合理使用泛型,提高代码复用性
  3. 使用类型断言要谨慎,优先使用类型收窄
  4. 充分利用IDE的类型提示功能
  5. 保持类型定义的一致性和集中管理

八、推荐工具和配置

  1. TypeScript 版本 >= 4.5
  2. VSCode + Volar
  3. ESLint + @typescript-eslint
  4. tsconfig.json 关键配置:
    • strict: true
    • noImplicitAny: true
    • strictNullChecks: true

参考资料


💡 开发中遇到 TypeScript 问题?欢迎评论区交流!

🔥 点赞收藏,持续更新 Vue3+TS 实战经验

#Vue3 #TypeScript #前端开发 #实战经验

相关推荐
zeijiershuai19 分钟前
Vue框架
前端·javascript·vue.js
写完这行代码打球去21 分钟前
没有与此调用匹配的重载
前端·javascript·vue.js
IT、木易26 分钟前
大白话Vue Router 中路由守卫(全局守卫、路由独享守卫、组件内守卫)的种类及应用场景
前端·javascript·vue.js
oil欧哟30 分钟前
🥳 做了三个月的学习卡盒小程序,开源了!
前端·vue.js·微信小程序
奶球不是球33 分钟前
el-table(elementui)表格合计行使用以及滚动条默认样式修改
前端·vue.js·elementui
liuyang___36 分钟前
vue管理布局左侧菜单栏NavMenu
前端·javascript·vue.js
destinying2 小时前
Vue 项目“瘦身”神器:自动清理未引用代码的终极方案
前端·javascript·vue.js
pany2 小时前
📱 MobVue 致力成为你的移动端 H5 首选
前端·javascript·vue.js
战场小包2 小时前
初探 Vite 秒级预构建实现
前端·vue.js·vite