前端 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 的 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 函数自动返回 Promise
- Promise 明确返回的数据类型
- 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 表示字符串集合。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,值是数组
- 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
复制代码 关键点:
- defineOptions 定义组件选项
- ref 自动推断类型
- 不需要显式标注简单类型
3.2 明确 ref 类型
- // src/views/system/user/index.vue
复制代码 关键点:
- ref(初始值) 明确类型
- 数组类型:ref([])
- 使用导入的 API 类型
3.3 reactive 定义复杂对象
- // src/views/system/user/index.vue
复制代码 关键点:
- reactive 用于复杂对象
- 自动推断类型
- 适合查询参数对象
3.4 defineProps 类型定义
- // src/views/system/user/UserForm.vue
复制代码- // src/views/system/companyTree/detail.vue
复制代码 关键点:
- 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
复制代码 关键点:
- (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
复制代码 关键点:
- FormRules 来自 element-plus
- reactive 定义验证规则
- as number | undefined 类型断言。手动告诉 TS 这个值的类型是 number | undefined(联合类型)。
- id: undefined as number | undefined 初始值是 undefined,但后面可能会被赋值成 number,所以类型标注为 number | undefined
3.7 组件 ref 类型
- // src/views/system/user/index.vue
复制代码 关键点:
- InstanceType 获取组件实例类型
- ref():创建一个响应式引用,类型是 T
- T 就是 InstanceType
- typeof ElTree:获取 ElTree 组件的构造函数类型
- InstanceType:从构造函数类型中提取实例类型
- ?. 可选链操作符,避免 undefined 报错
3.8 computed 计算属性类型
- // src/views/system/user/UserForm.vue
复制代码 关键点:
- 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, PageResult
- 在任何文件中都可以直接使用
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>),resolve 的值是 AxiosResponse<T>,而 response.data 的类型是 T。
- api?: <T = any>() => AxiosPromise<T>
- }
复制代码 关键点:
- type 定义类型别名
- 联合类型:string | number | boolean
- 泛型函数:() => AxiosPromise
五、实用工具函数的 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
- }
复制代码 关键点:
- (参数: 类型): 返回类型 => {}
- 泛型函数:
- 默认参数: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:
- 接口定义配置和状态
- 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 组件实例类型
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
- // 箭头函数
- 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
复制代码 案例 2:表单组件
- // src/views/system/user/UserForm.vue
复制代码 案例 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
3. Store 层
- ✅ 定义 State、Getters、Actions 的类型
- ✅ 使用 interface 定义 State 结构
- ✅ Getters 明确返回类型
- ✅ Actions 参数标注类型
4. Views 层
- ✅ 使用 ref() 或 reactive() 标注类型
- ✅ defineProps() 定义 Props 类型
- ✅ defineEmits() 定义 Emits 类型
- ✅ 使用 FormRules 定义表单验证
5. 工具函数
- ✅ 参数和返回值都要标注类型
- ✅ 复杂函数使用泛型
- ✅ 联合类型处理多种输入
6. 错误处理
- ✅ 使用可选链 ?. 避免 undefined 错误
- ✅ 使用空值合并 ?? 提供默认值
- ✅ 适时使用类型断言 as
- ⚠️ 避免过度使用 any
附录:常用类型速查表
类型说明示例string字符串const name: string = '张三'number数字const age: number = 20boolean布尔值const loading: boolean = truearray数组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 = nullundefined未定义const data: undefined = undefinednever永不返回const fn = (): never => { throw new Error() }PromisePromiseconst fn = (): Promise => {}RefVue Refconst count = ref(0)ComputedRefVue Computedconst double = computed(() => count.value * 2)Nullable可空类型const id: Nullable = nullRecordable对象const obj: Recordable = {}PageParam分页参数const params: PageParam = { pageNo: 1, pageSize: 10 }结束语
本文档覆盖了项目中 绝大多数 的 TypeScript 使用场景。建议:
- 先理解基础概念(interface、type、泛型)
- 在实际编写代码时参考对应章节
- 遇到错误时查看"常见错误和解决方案"
- 多看项目代码,模仿学习
TypeScript 的核心是类型安全,合理使用类型可以:
- ✅ 减少运行时错误
- ✅ 提升代码可维护性
- ✅ 提供更好的 IDE 智能提示
- ✅ 让代码更加规范
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |