前端 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: number
username: string
nickname: string
dept: {
id: number
name: string
}
// roles 是一个数组,数组里的每一项都是包含 id 和 name 两个字段的对象
roles: {
id: number
name: string
}[]
posts: {
id: number
name: string
}[]
email: string
sex: number
status: number
remark: string
createTime: Date
}
关键点:
[]表示数组类型,如roles: {...}[]表示角色数组- 嵌套对象直接在 interface 中定义,如
dept: { id: number; name: string } Date类型表示日期时间
1.2 可选属性与联合类型
// src/api/system/user/profile.ts
export interface UserProfileUpdateReqVO {
nickname?: string // ? 表示可选属性
email?: string
mobile?: string
sex?: number
avatar?: string
}
// src/api/system/sms/smsLog/index.ts
export interface SmsLogVO {
id: number | null // 联合类型:可以是 number 或 null
channelId: number | null
templateParams: Map<string, object> | null
sendStatus: number | null
sendTime: Date | null
}
关键点:
?表示该属性可选,可以不传|表示联合类型,值可以是多种类型之一null和undefined常用于表示空值Map<string, object>: 键是 string、值是 object 的 Map
1.3 接口继承(extends)
// src/api/system/tenant/index.ts
export interface TenantPageReqVO extends PageParam {
name?: string
contactName?: string
contactMobile?: string
status?: number
createTime?: Date[]
}
关键点:
extends PageParam继承了分页参数(pageNo, pageSize)- 继承后可以添加自己的属性
PageParam是全局类型,定义在types/global.d.ts
// global.d.ts
export {}
declare global {
interface PageParam {
pageSize?: number
pageNo?: 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函数自动返回 PromisePromise<PostVO[]>明确返回的数据类型await等待异步操作完成
二、Store(Pinia)层的 TypeScript 用法
2.1 定义 Store 的 State 类型
// src/store/modules/user.ts
export interface CompanyVO {
pid: string
companyName: string
isDefault: number
}
interface UserVO {
id: number
avatar: string
nickname: string
deptId: number
companyList: CompanyVO[]
sex?: number
position?: string
}
interface UserInfoVO {
permissions: Set<string> // Set 类型
roles: string[]
isSetUser: boolean
user: 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', {
// ... state
getters: {
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, getters
actions: {
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.roles
this.user = userInfo.user
},
async setUserAvatarAction(avatar: string) {
const userInfo = wsCache.get(CACHE_KEY.USER)
this.user.avatar = avatar
userInfo.user.avatar = avatar
wsCache.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: {
// 返回函数的 getter
getConversationMessageList(): (conversationId: number) => KeFuMessageRespVO[] | undefined {
return (conversationId: number) => this.conversationMessageList.get(conversationId)
}
/*
等价:
type GetMsgFn = (conversationId: number) => KeFuMessageRespVO[] | undefined
getConversationMessageList(): 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: boolean
breadcrumbIcon: boolean
collapse: boolean
uniqueOpened: boolean
hamburger: boolean
screenfull: boolean
size: boolean
locale: boolean
message: boolean
tagsView: boolean
tagsViewImmerse: boolean
tagsViewIcon: boolean
logo: boolean
fixedHeader: boolean
greyMode: boolean
pageLoading: boolean
layout: LayoutType
title: string
userInfo: string
isDark: boolean
currentSize: ElementPlusSize
sizeMap: ElementPlusSize[]
mobile: boolean
footer: boolean
theme: ThemeTypes
fixedMenu: 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): void
const 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-plusreactive<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
:从构造函数类型中提取实例类型
- ref
?.可选链操作符,避免 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?: number
pageNo?: number
}
// 分页结果
interface PageResult<T> {
list: T
total: number
}
// 树结构
interface Tree {
id: number
name: string
children?: 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 | null
export type FormItemProps = {
labelWidth?: string | number
required?: boolean
rules?: Recordable
error?: string
showMessage?: boolean
inlineMessage?: boolean
style?: CSSProperties
}
export type FormSchema = {
field: string
label?: string
labelMessage?: string
colProps?: 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 } & ComponentProps
formItemProps?: FormItemProps
component?: ComponentName
value?: FormValueType
hidden?: 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 = -1
ary.some((item: T, i: number, ary: Array<T>) => {
const ret: T = fn(item, i, ary)
if (ret) {
index = i
return 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?: ResponseType
defaultParams?: Recordable
props?: TableProps
}
interface TableObject<T = any> {
pageSize: number
currentPage: number
total: number
tableList: T[]
params: any
loading: boolean
exportLoading: boolean
currentRow: 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 = true
const res = await config?.getListApi(unref(paramsObj)).finally(() => {
tableObject.loading = false
})
if (res) {
// 不管 res 原本是什么类型,我强制把它当成 ResponseType 来用
tableObject.tableList = (res as unknown as ResponseType).list
tableObject.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 = ref
elTableRef.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 createUser
const 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: number
name: string
}
// 使用 Partial
type PartialUser = Partial<UserVO>
// 等价于手动写:
type PartialUser = {
id?: number
name?: 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: number
name: string
email: 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 类型
// ❌ 键必须是 number
const 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 = true
try {
const data = await UserApi.getUserPage(queryParams)
list.value = data.list
total.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 = true
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
formData.value = await UserApi.getUser(id)
} finally {
formLoading.value = false
}
}
// 加载岗位列表
postList.value = await PostApi.getSimplePostList()
}
// 提交表单
const submitForm = async () => {
const form = unref(formRef)
if (!form) return
await form.validate()
formLoading.value = true
try {
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 = false
emits('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: any
label: string
clorType?: string
cssClass?: string
}
export interface DictTypeType {
dictType: string
dictValue: 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 = dictMap
this.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 = dictDataMap
this.isSetDict = true
wsCache.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: string
age?: 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/await和Promise<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 使用场景。建议:
- 先理解基础概念(interface、type、泛型)
- 在实际编写代码时参考对应章节
- 遇到错误时查看"常见错误和解决方案"
- 多看项目代码,模仿学习
TypeScript 的核心是类型安全,合理使用类型可以:
- ✅ 减少运行时错误
- ✅ 提升代码可维护性
- ✅ 提供更好的 IDE 智能提示
- ✅ 让代码更加规范
出处:https://www.cnblogs.com/pengjiali/p/19387524
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。

浙公网安备 33010602011771号