ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

前端 TypeScript 入门2

前端 TypeScript 入门2

前端 TypeScript 入门2

在上一篇中,我们了解了 TS 常用语法,但是在Vue3项目实际开发中,会发现很多 TS 代码看不懂。本篇以实际 Vue3 项目为例,抽取出其中绝大多数 TS 常见写法,快速进入实战。

一、API 层的 TypeScript 用法

1.1 定义接口数据结构(interface)

在项目中,我们使用 interface 定义后端返回的数据结构。

// src/api/system/user/profile.ts
export interface ProfileVO {id: numberusername: stringnickname: stringdept: {id: numbername: string}// roles 是一个数组,数组里的每一项都是包含 id 和 name 两个字段的对象roles: {id: numbername: string}[]posts: {id: numbername: string}[]email: stringsex: numberstatus: numberremark: stringcreateTime: Date
}

关键点:

  • [] 表示数组类型,如 roles: {...}[] 表示角色数组
  • 嵌套对象直接在 interface 中定义,如 dept: { id: number; name: string }
  • Date 类型表示日期时间

1.2 可选属性与联合类型

// src/api/system/user/profile.ts
export interface UserProfileUpdateReqVO {nickname?: string // ? 表示可选属性email?: stringmobile?: stringsex?: numberavatar?: string
}// src/api/system/sms/smsLog/index.ts
export interface SmsLogVO {id: number | null // 联合类型:可以是 number 或 nullchannelId: number | nulltemplateParams: Map<string, object> | null sendStatus: number | nullsendTime: Date | null
}

关键点:

  • ? 表示该属性可选,可以不传
  • | 表示联合类型,值可以是多种类型之一
  • nullundefined 常用于表示空值
  • Map<string, object>: 键是 string、值是 object 的 Map

1.3 接口继承(extends)

// src/api/system/tenant/index.ts
export interface TenantPageReqVO extends PageParam {name?: stringcontactName?: stringcontactMobile?: stringstatus?: numbercreateTime?: Date[]
}

关键点:

  • extends PageParam 继承了分页参数(pageNo, pageSize)
  • 继承后可以添加自己的属性
  • PageParam 是全局类型,定义在 types/global.d.ts
// global.d.ts
export {}
declare global {interface PageParam {pageSize?: numberpageNo?: number}
}

TypeScript 是如何找到这个 global.d.ts 的:

{"compilerOptions": {// 告诉 TS:类型声明文件的根目录在这两个地方:"typeRoots": ["./node_modules/@types/", "./types"]},// 参与编译/类型检查的文件包括 types 目录下所有 .d.ts"include": ["src","types/**/*.d.ts","src/types/auto-imports.d.ts","src/types/auto-components.d.ts"]
}

1.4 API 函数的类型标注

// src/api/system/user/index.ts
// 查询用户详情 - 参数和返回值都有类型
export const getUser = (id: number) => {return request.get({ url: '/system/user/get?id=' + id })
}// 新增用户 - data 参数类型为 UserVO
export const createUser = (data: UserVO) => {return request.post({ url: '/system/user/create', data })
}// 修改用户
export const updateUser = (data: UserVO) => {return request.put({ url: '/system/user/update', data })
}// 批量删除 - 参数是数字数组
export const deleteUserList = (ids: number[]) => {return request.delete({ url: '/system/user/delete-list', params: { ids: ids.join(',') } })
}

关键点:

  • 参数类型写在参数名后面:(id: number)
  • 对象类型参数用 interface:(data: UserVO)
  • 数组类型:ids: number[]

1.5 async/await 与 Promise 类型

// src/api/system/post/index.ts
// 异步返回一个 Promise,这个 Promise 最终会 resolve 成 PostVO 对象组成的数组。
// 返回 Promise<PostVO[]>。
export const getSimplePostList = async (): Promise<PostVO[]> => {return await request.get({ url: '/system/post/simple-list' })
}// 查询详情
export const getPost = async (id: number) => {return await request.get({ url: '/system/post/get?id=' + id })
}// 删除
export const deletePost = async (id: number) => {return await request.delete({ url: '/system/post/delete?id=' + id })
}

关键点:

  • async 函数自动返回 Promise
  • Promise<PostVO[]> 明确返回的数据类型
  • await 等待异步操作完成

二、Store(Pinia)层的 TypeScript 用法

2.1 定义 Store 的 State 类型

// src/store/modules/user.ts
export interface CompanyVO {pid: stringcompanyName: stringisDefault: number
}interface UserVO {id: numberavatar: stringnickname: stringdeptId: numbercompanyList: CompanyVO[]sex?: numberposition?: string
}interface UserInfoVO {permissions: Set<string> // Set 类型roles: string[]isSetUser: booleanuser: UserVO
}export const useUserStore = defineStore('admin-user', {state: (): UserInfoVO => ({permissions: new Set<string>(),roles: [],isSetUser: false,user: {id: 0,avatar: '',nickname: '',deptId: 0,companyList: []}})// ... getters 和 actions
})

关键点:

  • state: (): UserInfoVO => (...) 定义 state 返回类型是 UserInfoVO。主要是为了类型检查和提示,不一定会被你显式“拿出来用”。
  • Set<string> 表示字符串集合。Set 中的值必须是 string 类型。

2.2 Getters 的类型

// src/store/modules/user.ts
export const useUserStore = defineStore('admin-user', {// ... stategetters: {getPermissions(): Set<string> {return this.permissions},getRoles(): string[] {return this.roles},getIsSetUser(): boolean {return this.isSetUser},getUser(): UserVO {return this.user}}
})

关键点:

  • getter 函数后面标注返回类型
  • 使用 this 访问 state

2.3 Actions 的类型标注

// src/store/modules/user.ts
export const useUserStore = defineStore('admin-user', {// ... state, gettersactions: {async setUserInfoAction() {if (!getAccessToken()) {this.resetState()return null}let userInfo = wsCache.get(CACHE_KEY.USER)if (!userInfo) {userInfo = await getInfo()}this.permissions = new Set(userInfo.permissions || [])this.roles = userInfo.rolesthis.user = userInfo.user},async setUserAvatarAction(avatar: string) {const userInfo = wsCache.get(CACHE_KEY.USER)this.user.avatar = avataruserInfo.user.avatar = avatarwsCache.set(CACHE_KEY.USER, userInfo)},async loginOut() {try {await loginOut()} catch (error) {console.error('登出接口调用失败:', error)} finally {removeToken()deleteUserCache()this.resetState()}}}
})

关键点:

  • action 函数参数需要类型:(avatar: string)
  • async action 返回 Promise
  • 通过 this 修改 state

2.4 Map 类型的使用

// src/store/modules/mall/kefu.ts
interface MallKefuInfoVO {conversationList: KeFuConversationRespVO[]conversationMessageList: Map<number, KeFuMessageRespVO[]> // Map 类型
}export const useMallKefuStore = defineStore('mall-kefu', {state: (): MallKefuInfoVO => ({conversationList: [],conversationMessageList: new Map<number, KeFuMessageRespVO[]>()}),getters: {// 返回函数的 gettergetConversationMessageList(): (conversationId: number) => KeFuMessageRespVO[] | undefined {return (conversationId: number) => this.conversationMessageList.get(conversationId)}/*等价:type GetMsgFn = (conversationId: number) => KeFuMessageRespVO[] | undefinedgetConversationMessageList(): GetMsgFn {return (conversationId: number) => this.conversationMessageList.get(conversationId)}*/},actions: {saveMessageList(conversationId: number, messageList: KeFuMessageRespVO[]) {this.conversationMessageList.set(conversationId, messageList)}}
})

关键点:

  • Map<number, KeFuMessageRespVO[]> 键是 number,值是数组
  • getter 可以返回函数
  • 使用 .get().set() 操作 Map
  • (conversationId: number) => KeFuMessageRespVO[] | undefined: 类型是函数,返回值是KeFuMessageRespVO[]或undefined

2.5 复杂 State 定义

// src/store/modules/app.ts
interface AppState {breadcrumb: booleanbreadcrumbIcon: booleancollapse: booleanuniqueOpened: booleanhamburger: booleanscreenfull: booleansize: booleanlocale: booleanmessage: booleantagsView: booleantagsViewImmerse: booleantagsViewIcon: booleanlogo: booleanfixedHeader: booleangreyMode: booleanpageLoading: booleanlayout: LayoutTypetitle: stringuserInfo: stringisDark: booleancurrentSize: ElementPlusSizesizeMap: ElementPlusSize[]mobile: booleanfooter: booleantheme: ThemeTypesfixedMenu: boolean
}export const useAppStore = defineStore('app', {state: (): AppState => {return {userInfo: 'userInfo',sizeMap: ['default', 'large', 'small'],mobile: false,title: import.meta.env.VITE_APP_TITLE,pageLoading: false// ... 其他属性}}
})

关键点:

  • 布尔类型属性集中定义
  • 自定义类型:LayoutType, ElementPlusSize, ThemeTypes
  • 使用 import.meta.env 获取环境变量

三、Views(Vue 组件)层的 TypeScript 用法

3.1 defineOptions 和 ref

// src/views/system/role/index.vue
<script lang="ts" setup>
import * as RoleApi from '@/api/system/role'defineOptions({ name: 'SystemRole' })const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化const loading = ref(true)  // 布尔值
const total = ref(0)       // 数字
const list = ref([])       // 数组
</script>

关键点:

  • defineOptions 定义组件选项
  • ref 自动推断类型
  • 不需要显式标注简单类型

3.2 明确 ref 类型

// src/views/system/user/index.vue
<script lang="ts" setup>
import * as UserApi from '@/api/system/user'const loading = ref(true)
const total = ref(0)
const list = ref([])  // 可以推断,但不明确// 更好的写法:明确类型
const loading = ref<boolean>(true)
const total = ref<number>(0)
const list = ref<UserApi.UserVO[]>([])  // 明确是 UserVO 数组
const companyTreeData = ref<Tree[]>([])  // Tree 是全局类型
</script>

关键点:

  • ref<类型>(初始值) 明确类型
  • 数组类型:ref<UserVO[]>([])
  • 使用导入的 API 类型

3.3 reactive 定义复杂对象

// src/views/system/user/index.vue
<script lang="ts" setup>
const queryParams = reactive({pageNo: 1,pageSize: 10,username: undefined,mobile: undefined,status: undefined,deptId: undefined,createTime: []
})
</script>

关键点:

  • reactive 用于复杂对象
  • 自动推断类型
  • 适合查询参数对象

3.4 defineProps 类型定义

// src/views/system/user/UserForm.vue
<script lang="ts" setup>
// 方式 1:使用 defineProps<>()
const props = defineProps<{companyTreeData?: any[]fixedCompany?: { name: string; pid: number } | null
}>()// 访问 props
const companyTreeData = computed(() => props.companyTreeData || [])
const fixedCompany = computed(() => props.fixedCompany || null)
</script>
// src/views/system/companyTree/detail.vue
<script setup lang="ts">
import { ref, defineProps } from 'vue'// 方式 2:定义带默认值的 props
const props = defineProps({title: {type: String,default: ''}
})
</script>

关键点:

  • defineProps<{}>() 泛型方式定义 props
  • 可选属性用 ?
  • 联合类型:{ name: string; pid: number } | null
  • computed 包装 props 进行计算

defineProps语法:

// 孩子没脾气
defineProps(['persons'])// 接收+限制类型
defineProps<{persons:Persons}>()// 接收+限制类型+限制必要性 —— 可以不传
defineProps<{persons?:Persons}>()// 接收+限制类型+限制必要性+默认值
import {withDefaults} from 'vue'
withDefaults(defineProps<{persons?:Persons}>(), {persons: () => []
})

3.5 defineEmits 类型定义

// src/views/system/user/CompanyTree.vue
<script lang="ts" setup>
// 定义 emit 类型
const emits = defineEmits<{(e: 'node-click', row: any): void
}>()// 触发事件
const handleNodeClick = (row: any) => {emits('node-click', row)
}
</script>

关键点:

  • (e: '事件名', 参数: 类型): void
  • 可以定义多个事件
  • 使用 emits('事件名', 参数) 触发

声明一个名为 'node-click'的事件的三种写法(区别是类型检查严格程度):

defineEmits(['node-click'])// 定义一个名为 'node-click'​ 的事件。这是一个函数签名形式的定义
(e: 'node-click', row: any): voidconst emit = defineEmits<{// 新语法:'事件名': [参数1类型, 参数2类型?]'node-click': [row: any]  
}>();

3.6 FormRules 表单验证类型

// src/views/system/user/ResetPasswordForm.vue
<script lang="ts" setup>
import { FormRules } from 'element-plus'const formData = ref({id: undefined as number | undefined,username: '',password: ''
})const formRules = reactive<FormRules>({password: [{ required: true, message: '新密码不能为空', trigger: 'blur' },{ min: 6, max: 16, message: '密码长度为 6-16 位', trigger: 'blur' },{pattern: /^(...$/,message: '密码需包含...',trigger: 'blur'}]
})
</script>

关键点:

  • FormRules 来自 element-plus
  • reactive<FormRules> 定义验证规则
  • as number | undefined 类型断言。手动告诉 TS 这个值的类型是 number | undefined(联合类型)。
  • id: undefined as number | undefined 初始值是 undefined,但后面可能会被赋值成 number,所以类型标注为 number | undefined

3.7 组件 ref 类型

// src/views/system/user/index.vue
<script lang="ts" setup>
import { ElTree } from 'element-plus'const treeRef = ref<InstanceType<typeof ElTree>>()// 调用组件方法
treeRef.value?.filter(val)
</script>

关键点:

  • InstanceType<typeof ElTree> 获取组件实例类型
    • ref():创建一个响应式引用,类型是 T
    • T 就是 InstanceType
    • typeof ElTree:获取 ElTree 组件的构造函数类型
    • InstanceType:从构造函数类型中提取实例类型
  • ?. 可选链操作符,避免 undefined 报错

3.8 computed 计算属性类型

// src/views/system/user/UserForm.vue
<script lang="ts" setup>
const props = defineProps<{companyTreeData?: any[]
}>()// 自动推断返回类型
const companyTreeData = computed(() => props.companyTreeData || [])// 明确返回类型
const isValid = computed<boolean>(() => {return formData.value.password.length >= 8
})
</script>

关键点:

  • computed(() => {}) 自动推断
  • computed<类型>(() => {}) 明确类型

四、全局类型定义(types)

4.1 全局工具类型

// types/global.d.ts
declare global {// 函数类型interface Fn<T = any> {(...arg: T[]): T}// 可空类型/*- type Xxx = ...:和 ype Message = number | string 一样,都是起一个类型别名- T:是一个类型参数,占位的,不是具体类型- 示例:- type Nullable<T> = T | null- type A = Nullable<string>   // string | null- type B = Nullable<number>   // number | null*/type Nullable<T> = T | null// 元素引用类型。声明一个“可以为 null 的 DOM 引用类型”,T 必须是 HTMLElement 的子类型,默认用 HTMLDivElement。type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>// 记录类型(对象)/*如果不考虑 null/undefined 的边界情况,可以简化成:type Recordable<T = any, K = string> = Record<K, T>- Record<K, V> 是 TypeScript 内置的工具类型- 一个对象,键的类型是 K,值的类型是 V- 示例:type User = Record<string, any>// 等价于:{ [key: string]: any }const user: User = {name: '张三',age: 18,job: '工程师'}- 条件类型:K extends null | undefined ? string : K- 如果 K 是 null 或 undefined → 键类型用 string- 否则 → 键类型就用 K 本身- 最终结果:Record<键类型, T>*/type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T>// 组件引用类型/*做一层语义封装,让代码更清晰。不用别名:const dialogRef = ref<InstanceType<typeof ElDialog>>()const tableRef = ref<InstanceType<typeof ElTable>>()用别名(用 ComponentRef):const dialogRef = ref<ComponentRef<typeof ElDialog>>()const tableRef = ref<ComponentRef<typeof ElTable>>()*/type ComponentRef<T> = InstanceType<T>// 分页参数interface PageParam {pageSize?: numberpageNo?: number}// 分页结果interface PageResult<T> {list: Ttotal: number}// 树结构interface Tree {id: numbername: stringchildren?: Tree[] | any[]}
}

关键点:

  • declare global 声明全局类型
  • 泛型类型:Nullable<T>, PageResult<T>
  • 在任何文件中都可以直接使用

4.2 表单 Schema 类型

// src/types/form.d.ts
export type FormValueType = string | number | string[] | number[] | boolean | undefined | nullexport type FormItemProps = {labelWidth?: string | numberrequired?: booleanrules?: Recordableerror?: stringshowMessage?: booleaninlineMessage?: booleanstyle?: CSSProperties
}export type FormSchema = {field: stringlabel?: stringlabelMessage?: stringcolProps?: ColProps/*定义一个可选属性 componentProps,它的类型是两个类型的交叉- componentProps?:可选属性- { slots?: Recordable } & ComponentProps:交叉类型(&)- 交叉示例:type A = { name: string }type B = { age: number }type C = A & B  // { name: string; age: number }const obj: C = {name: '张三',age: 18}- { slots?: Recordable } & ComponentProps- 必须包含 ComponentProps 里的所有属性- 同时可以有一个可选的 slots 属性,类型是 Recordable(也就是 Record<string, any>)*/componentProps?: { slots?: Recordable } & ComponentPropsformItemProps?: FormItemPropscomponent?: ComponentNamevalue?: FormValueTypehidden?: boolean// 返回值是 AxiosPromise<T>(也就是 Promise<AxiosResponse<T>>),resolve 的值是 AxiosResponse<T>,而 response.data 的类型是 T。api?: <T = any>() => AxiosPromise<T>
}

关键点:

  • type 定义类型别名
  • 联合类型:string | number | boolean
  • 泛型函数:<T = any>() => AxiosPromise<T>

五、实用工具函数的 TypeScript

5.1 函数参数和返回值类型

// src/utils/index.ts// 字符串转换
export const humpToUnderline = (str: string): string => {return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}// 设置 CSS 变量
export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {dom.style.setProperty(prop, val)
}// 数组查找(泛型函数)
export const findIndex = <T = Recordable>(ary: Array<T>, fn: Fn): number => {if (ary.findIndex) {return ary.findIndex(fn)}let index = -1ary.some((item: T, i: number, ary: Array<T>) => {const ret: T = fn(item, i, ary)if (ret) {index = ireturn ret}})return index
}

关键点:

  • (参数: 类型): 返回类型 => {}
  • 泛型函数:<T = Recordable>
  • 默认参数:dom = document.documentElement

5.2 时间格式化函数

// src/utils/index.ts
export function formatTime(time: Date | number | string, fmt: string) {if (!time) return ''else {const date = new Date(time)const o = {'M+': date.getMonth() + 1,'d+': date.getDate(),'H+': date.getHours(),'m+': date.getMinutes(),'s+': date.getSeconds(),'q+': Math.floor((date.getMonth() + 3) / 3),S: date.getMilliseconds()}// ... 格式化逻辑return fmt}
}

关键点:

  • 联合类型参数:Date | number | string
  • 对象字面量类型自动推断

5.3 数字和金额处理

// src/utils/index.ts// 数组求和
export const getSumValue = (values: number[]): number => {return values.reduce((prev, curr) => {const value = Number(curr)if (!Number.isNaN(value)) {return prev + curr} else {return prev}}, 0)
}// 元转分
export const yuanToFen = (amount: string | number): number => {return convertToInteger(amount)
}// 分转元
export const fenToYuan = (price: string | number): string => {return formatToFraction(price)
}// ERP 格式化数字
export const erpNumberFormatter = (num: number | string | undefined, digit: number) => {if (num == null) {return ''}if (typeof num === 'string') {num = parseFloat(num)}if (isNaN(num)) {return ''}return num.toFixed(digit)
}

关键点:

  • 参数支持多种类型:string | number
  • 返回值类型明确:: number: string
  • undefined 处理

六、Hooks(组合式函数)的 TypeScript

6.1 useTable Hook

// src/hooks/web/useTable.ts
interface ResponseType<T = any> {list: T[]total?: number
}interface UseTableConfig<T = any> {getListApi: (option: any) => Promise<T>delListApi?: (option: any) => Promise<T>exportListApi?: (option: any) => Promise<T>response?: ResponseTypedefaultParams?: Recordableprops?: TableProps
}interface TableObject<T = any> {pageSize: numbercurrentPage: numbertotal: numbertableList: T[]params: anyloading: booleanexportLoading: booleancurrentRow: Nullable<T>
}
/*- reactive<TableObject<T>> 表示创建一个响应式对象,它的类型是 TableObject<T>- 初始值是 { pageSize: 10, ... },必须符合 TableObject<T> 的结构
*/
export const useTable = <T = any>(config?: UseTableConfig<T>) => {const tableObject = reactive<TableObject<T>>({pageSize: 10,currentPage: 1,total: 10,tableList: [],params: {...(config?.defaultParams || {})},loading: true,exportLoading: false,currentRow: null})const paramsObj = computed(() => {return {...tableObject.params,pageSize: tableObject.pageSize,pageNo: tableObject.currentPage}})const methods = {getList: async () => {tableObject.loading = trueconst res = await config?.getListApi(unref(paramsObj)).finally(() => {tableObject.loading = false})if (res) {// 不管 res 原本是什么类型,我强制把它当成 ResponseType 来用tableObject.tableList = (res as unknown as ResponseType).listtableObject.total = (res as unknown as ResponseType).total ?? 0}}// ... 其他方法}return {tableObject,methods}
}

关键点:

  • 泛型 Hook:<T = any>
  • 接口定义配置和状态
  • reactive<类型> 定义响应式对象
  • 双重类型断言:as unknown as ResponseType
    • 先断言成 unknown
    • 再断言成 ResponseType
    • 用来强制转换原本不兼容的类型,绕过 TS 检查

6.2 组件引用类型

// src/hooks/web/useTable.ts
import { ElTable } from 'element-plus'
/*它是Table 组件实例 + TableExpose 接口的交叉类型既是 Table 组件的实例,又包含 TableExpose 里定义的方法。等价于:type TableRef = InstanceType<typeof Table> & TableExpose
*/
const tableRef = ref<typeof Table & TableExpose>()
const elTableRef = ref<ComponentRef<typeof ElTable>>()const register = (ref: typeof Table & TableExpose, elRef: ComponentRef<typeof ElTable>) => {tableRef.value = refelTableRef.value = elRef
}

关键点:

  • typeof 获取类型
  • & 交叉类型(同时满足多个类型)
  • ComponentRef<T> 组件实例类型

typeof 用法:

function createUser(name: string, age: number) {return {name,age,sayHello() {console.log(`Hi, I'm ${name}`)}}
}type CreateUserFn = typeof createUserconst myCreateUser: CreateUserFn = (n, a) => {return { age: a, sayHello() {} }  // ❌ 报错。缺少name
}

七、常见类型使用技巧

7.1 类型断言

// 断言为特定类型
const value = someValue as string// 先断言为 unknown,再断言为目标类型
const data = res as unknown as ResponseType// 表单数据的类型断言
/*- formData.value.id:某个表单数据的 id 字段- undefined:赋的值是 undefined- as number | undefined:告诉 TS,这个 id 的类型是 number | undefined如果直接写:formData.value.id = undefined,TS 可能推断 id 的类型是 undefined,后面你想给它赋数字时会报错其实不用断言:id: number | undefined
*/
formData.value.id = undefined as number | undefined

7.2 可选链和空值合并

// 可选链:?.
treeRef.value?.filter(val)
config?.getListApi(params)// 空值合并:??
const total = response.total ?? 0
const lang = wsCache.get(CACHE_KEY.LANG) || 'zh-CN'

7.3 数组和对象的类型

// 数组类型
const list: string[] = []
const roles: number[] = [1, 2, 3]
const users: UserVO[] = []// 对象类型
const obj: { [key: string]: any } = {}
const params: Recordable = {}// Map 和 Set
const map = new Map<string, UserVO>()
const set = new Set<string>()

7.4 函数类型

// 函数类型定义
/*Callback 是一个函数类型,这个函数接收一个 data 参数(任意类型),不返回任何值- (data: any) => void:这是一个函数类型- 示例:type Callback = (data: any) => void// 1. 定义一个符合 Callback 类型的函数const handleSuccess: Callback = (data) => {console.log('成功:', data)}
*/
type Callback = (data: any) => void
type ApiFunction = (params: any) => Promise<any>// 箭头函数
const handleClick = (id: number): void => {console.log(id)
}// async 函数
const fetchData = async (id: number): Promise<UserVO> => {const res = await api.getUser(id)return res
}

八、项目中的高级 TypeScript 用法

8.1 泛型约束

// 约束泛型必须包含某些属性
/*K extends keyof T:K 必须是 T 的键之一。- 不安全的写法:function getPropertyUnsafe(obj: any, key: string) {return obj[key]  // 返回值类型是 any,不安全}const value = getPropertyUnsafe(user, 'xxx')  // ✅ 不报错,但运行时可能 undefined
*/
function getProperty<T, K extends keyof T>(obj: T, key: K) {return obj[key]
}// 使用示例
const user = { name: '张三', age: 20 }
const name = getProperty(user, 'name') // OK
const gender = getProperty(user, 'gender') // 错误:gender 不在 user 中

8.2 工具类型

// Partial:所有属性变为可选
/*// 原始类型interface UserVO {id: numbername: string}// 使用 Partialtype PartialUser = Partial<UserVO>// 等价于手动写:type PartialUser = {id?: numbername?: string}
*/
type PartialUser = Partial<UserVO>// Required:所有属性变为必填
type RequiredUser = Required<UserVO>// Pick:选择部分属性
type UserBasic = Pick<UserVO, 'id' | 'username' | 'nickname'>// Omit:排除部分属性
type UserWithoutPassword = Omit<UserVO, 'password'>// Record:创建对象类型
/*使用 TypeScript 内置工具类型 Record<K, V> 创建一个"键值对映射"类型- Record<K, V>:创建一个对象类型- Record<number, UserVO>等价于手动写:type UserMap = {[key: number]: UserVO}示例:interface UserVO {id: numbername: stringemail: string}type UserMap = Record<number, UserVO>// 使用const users: UserMap = {1: { id: 1, name: '张三', email: 'zhang@example.com' },2: { id: 2, name: '李四', email: 'li@example.com' },100: { id: 100, name: '王五', email: 'wang@example.com' }}// 访问const user1 = users[1]      // UserVO 类型const user2 = users[2]      // UserVO 类型// ❌ 键必须是 numberconst invalid: UserMap = {'abc': { id: 1, name: '张三', email: 'zhang@example.com' }  // 报错}
*/
type UserMap = Record<number, UserVO>

8.3 条件类型

// 根据条件选择类型
type IsString<T> = T extends string ? true : false// 使用示例
type A = IsString<string> // true
type B = IsString<number> // false// 项目中的使用
type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T>

8.4 模板字面量类型

// 动态生成类型
type EventName = 'click' | 'change' | 'input'
// Capitalize<T> 是 TS 内置工具类型:type A = Capitalize<'click'>   // 'Click'
type EventHandler = `on${Capitalize<EventName>}`
// 结果:'onClick' | 'onChange' | 'onInput'

九、实战案例

案例 1:用户管理页面

// src/views/system/user/index.vue
<script lang="ts" setup>
import * as UserApi from '@/api/system/user'
import * as CompanyApi from '@/api/system/company'defineOptions({ name: 'SystemUser' })const message = useMessage()
const { t } = useI18n()// 列表状态
const loading = ref<boolean>(true)
const total = ref<number>(0)
const list = ref<UserApi.UserVO[]>([])
const companyTreeData = ref<Tree[]>([])// 查询参数
const queryParams = reactive({pageNo: 1,pageSize: 10,username: undefined as string | undefined,mobile: undefined as string | undefined,status: undefined as number | undefined,deptId: undefined as number | undefined,createTime: [] as Date[]
})// 获取用户列表
const getList = async () => {loading.value = truetry {const data = await UserApi.getUserPage(queryParams)list.value = data.listtotal.value = data.total} finally {loading.value = false}
}// 删除用户
const handleDelete = async (id: number) => {await message.delConfirm()await UserApi.deleteUser(id)message.success(t('common.delSuccess'))await getList()
}// 组件挂载后加载数据
onMounted(() => {getList()
})
</script>

案例 2:表单组件

// src/views/system/user/UserForm.vue
<script lang="ts" setup>
import { FormRules } from 'element-plus'
import * as UserApi from '@/api/system/user'
import * as PostApi from '@/api/system/post'const { t } = useI18n()
const message = useMessage()// Props
const props = defineProps<{companyTreeData?: any[]fixedCompany?: { name: string; pid: number } | null
}>()// Emits
const emits = defineEmits<{(e: 'success'): void
}>()// 表单数据
const formData = ref<UserApi.UserVO>({id: undefined,username: '',nickname: '',password: '',deptId: undefined,postIds: [],email: '',mobile: '',sex: undefined,status: 0
})// 表单验证规则
const formRules = reactive<FormRules>({username: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],nickname: [{ required: true, message: '...', trigger: 'blur' }],password: [{ required: true, message: '密码不能为空', trigger: 'blur' },{ min: 8, max: 16, message: '...', trigger: 'blur' }],email: [{ required: true, message: '邮箱不能为空', trigger: 'blur' },{ type: 'email', message: '...', trigger: 'blur' }],mobile: [{ required: true, message: '手机号不能为空', trigger: 'blur' },{ pattern: /^1[3-9]\d{9}$/, message: '...', trigger: 'blur' }]
})const formRef = ref()
const dialogVisible = ref<boolean>(false)
const formLoading = ref<boolean>(false)
const formType = ref<'create' | 'update'>('create')
const postList = ref<PostApi.PostVO[]>([])// 打开表单
const open = async (type: 'create' | 'update', id?: number) => {dialogVisible.value = trueformType.value = typeresetForm()if (id) {formLoading.value = truetry {formData.value = await UserApi.getUser(id)} finally {formLoading.value = false}}// 加载岗位列表postList.value = await PostApi.getSimplePostList()
}// 提交表单
const submitForm = async () => {const form = unref(formRef)if (!form) returnawait form.validate()formLoading.value = truetry {const data = unref(formData)if (formType.value === 'create') {await UserApi.createUser(data)message.success(t('common.createSuccess'))} else {await UserApi.updateUser(data)message.success(t('common.updateSuccess'))}dialogVisible.value = falseemits('success')} finally {formLoading.value = false}
}// 重置表单
const resetForm = () => {formData.value = {id: undefined,username: '',nickname: '',password: '',deptId: undefined,postIds: [],email: '',mobile: '',sex: undefined,status: 0}formRef.value?.resetFields()
}defineExpose({ open })
</script>

案例 3:Pinia Store

// src/store/modules/dict.ts
import { defineStore } from 'pinia'
import { store } from '../index'
import { DictDataVO } from '@/api/system/dict/types'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { getSimpleDictDataList } from '@/api/system/dict/dict.data'const { wsCache } = useCache('sessionStorage')export interface DictValueType {value: anylabel: stringclorType?: stringcssClass?: string
}export interface DictTypeType {dictType: stringdictValue: DictValueType[]
}export interface DictState {dictMap: Map<string, any>isSetDict: boolean
}export const useDictStore = defineStore('dict', {state: (): DictState => ({dictMap: new Map<string, any>(),isSetDict: false}),getters: {getDictMap(): Recordable {const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)if (dictMap) {this.dictMap = dictMap}return this.dictMap},getIsSetDict(): boolean {return this.isSetDict}},actions: {async setDictMap() {const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)if (dictMap) {this.dictMap = dictMapthis.isSetDict = true} else {const res = await getSimpleDictDataList()const dictDataMap = new Map<string, any>()res.forEach((dictData: DictDataVO) => {const enumValueObj = dictDataMap[dictData.dictType]if (!enumValueObj) {dictDataMap[dictData.dictType] = []}dictDataMap[dictData.dictType].push({value: dictData.value,label: dictData.label,colorType: dictData.colorType,cssClass: dictData.cssClass})})this.dictMap = dictDataMapthis.isSetDict = truewsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 })}},getDictByType(type: string) {if (!this.isSetDict) {this.setDictMap()}return this.dictMap[type]},async resetDict() {wsCache.delete(CACHE_KEY.DICT_CACHE)await this.setDictMap()}}
})export const useDictStoreWithOut = () => {return useDictStore(store)
}

十、常见错误和解决方案

错误 1:类型 "xxx" 上不存在属性 "yyy"

// 错误示例
const user = { name: '张三' }
console.log(user.age) // 错误:类型"{name: string}"上不存在属性"age"// 解决方案 1:定义完整类型
interface User {name: stringage?: number // 可选属性
}
const user: User = { name: '张三' }
console.log(user.age) // OK// 解决方案 2:类型断言
console.log((user as any).age) // OK,但不推荐

错误 2:类型 "undefined" 不能赋值给类型 "xxx"

// 错误示例
const formData = ref<UserVO>({}) // 错误// 解决方案:属性设为可选或提供默认值
const formData = ref<Partial<UserVO>>({}) // 使用 Partial// 或者提供完整默认值
const formData = ref<UserVO>({id: undefined,username: '',nickname: ''
})

错误 3:参数 "xxx" 隐式具有 "any" 类型

// 错误示例
const handleClick = (item) => {// 错误console.log(item.id)
}// 解决方案:显式标注类型
const handleClick = (item: UserVO) => {console.log(item.id)
}

错误 4:无法调用可能是 "undefined" 的对象

// 错误示例
formRef.value.validate() // 错误:可能是 undefined// 解决方案 1:可选链
formRef.value?.validate()// 解决方案 2:判断后调用
if (formRef.value) {formRef.value.validate()
}// 解决方案 3:非空断言(确定不为空时使用)
// 我向 TypeScript 保证:在这里 formRef.value 一定不是 null 或 undefined,你放心当成非空来用。
formRef.value!.validate()

十一、最佳实践总结

1. 类型定义原则

  • ✅ 优先使用 interface 定义对象结构
  • ✅ 使用 type 定义联合类型、交叉类型
  • ✅ 简单类型可以让 TS 自动推断
  • ✅ 复杂类型明确标注

2. API 层

  • ✅ 为每个接口定义 VO/DTO 类型
  • ✅ API 函数参数和返回值标注类型
  • ✅ 使用 async/awaitPromise<T>

3. Store 层

  • ✅ 定义 State、Getters、Actions 的类型
  • ✅ 使用 interface 定义 State 结构
  • ✅ Getters 明确返回类型
  • ✅ Actions 参数标注类型

4. Views 层

  • ✅ 使用 ref<T>()reactive<T>() 标注类型
  • defineProps<{}>() 定义 Props 类型
  • defineEmits<{}>() 定义 Emits 类型
  • ✅ 使用 FormRules 定义表单验证

5. 工具函数

  • ✅ 参数和返回值都要标注类型
  • ✅ 复杂函数使用泛型
  • ✅ 联合类型处理多种输入

6. 错误处理

  • ✅ 使用可选链 ?. 避免 undefined 错误
  • ✅ 使用空值合并 ?? 提供默认值
  • ✅ 适时使用类型断言 as
  • ⚠️ 避免过度使用 any

附录:常用类型速查表

类型 说明 示例
string 字符串 const name: string = '张三'
number 数字 const age: number = 20
boolean 布尔值 const loading: boolean = true
array 数组 const list: string[] = []
object 对象 const obj: { id: number } = { id: 1 }
any 任意类型 const data: any = {}
unknown 未知类型 const data: unknown = {}
void 无返回值 const fn = (): void => {}
null const data: null = null
undefined 未定义 const data: undefined = undefined
never 永不返回 const fn = (): never => { throw new Error() }
Promise<T> Promise const fn = (): Promise<string> => {}
Ref<T> Vue Ref const count = ref<number>(0)
ComputedRef<T> Vue Computed const double = computed<number>(() => count.value * 2)
Nullable<T> 可空类型 const id: Nullable<number> = null
Recordable 对象 const obj: Recordable = {}
PageParam 分页参数 const params: PageParam = { pageNo: 1, pageSize: 10 }

结束语

本文档覆盖了项目中 绝大多数 的 TypeScript 使用场景。建议:

  1. 先理解基础概念(interface、type、泛型)
  2. 在实际编写代码时参考对应章节
  3. 遇到错误时查看"常见错误和解决方案"
  4. 多看项目代码,模仿学习

TypeScript 的核心是类型安全,合理使用类型可以:

  • ✅ 减少运行时错误
  • ✅ 提升代码可维护性
  • ✅ 提供更好的 IDE 智能提示
  • ✅ 让代码更加规范
返回列表