工具类型
介绍
工具类型(Utility Types)是 TypeScript 提供的一组强大的类型转换工具,用于从现有类型构造新类型。RuoYi-Plus-UniApp 前端项目充分利用了 TypeScript 内置工具类型,并在此基础上定义了一套项目特定的工具类型系统,为 API 交互、UI 控制、数据处理等场景提供完整的类型安全保障。
通过合理使用工具类型,可以显著提升代码的类型安全性、可维护性和开发效率。工具类型让我们能够基于已有类型灵活派生出新类型,避免重复定义,实现类型复用和组合。
核心价值:
- 类型安全 - 编译时发现潜在错误,减少运行时异常
- 代码复用 - 基于已有类型派生新类型,避免重复定义
- 自动推导 - 充分利用 TypeScript 的类型推导能力
- 文档作用 - 类型即文档,清晰表达数据结构和约束
- 重构友好 - 类型系统辅助重构,降低改动风险
- 智能提示 - IDE 自动补全和类型提示,提升开发体验
TypeScript 内置工具类型
属性修饰工具类型
这类工具类型用于修改对象类型的属性特性,如可选/必选、只读等。
Partial - 所有属性可选
将类型 T 的所有属性转换为可选属性。
type Partial<T> = {
[P in keyof T]?: T[P]
}使用场景:
- 表单初始化 - 表单字段可能只填写部分
- 数据更新 - 只更新部分字段,其他保持不变
- 配置选项 - 允许用户只配置部分选项
代码示例:
import type { UserVo } from '@/api/system/user/types'
// 完整的用户对象
interface UserVo {
userId: string
userName: string
nickName: string
email: string
phonenumber: string
sex: string
avatar: string
status: string
createTime: string
}
// 表单数据(部分字段可选)
const formData: Partial<UserVo> = {
userName: 'admin',
nickName: '管理员'
// 其他字段可以不填
}
// 更新用户信息(只更新部分字段)
const updateData: Partial<UserVo> = {
nickName: '超级管理员',
email: 'admin@example.com'
}
// 函数参数使用 Partial
function updateUser(userId: string, data: Partial<UserVo>) {
// 只更新提供的字段
}
updateUser('1', { nickName: '新昵称' })最佳实践:
- ✅ 用于表单编辑场景,允许只修改部分字段
- ✅ 用于配置对象,提供默认值
- ❌ 避免过度使用,可能导致类型过于宽松
Required - 所有属性必选
将类型 T 的所有属性转换为必选属性,移除可选修饰符 ?。
type Required<T> = {
[P in keyof T]-?: T[P]
}使用场景:
- 数据提交 - 确保所有必填字段都已提供
- 严格校验 - 强制要求完整数据
- 配置校验 - 验证配置对象完整性
代码示例:
interface UserForm {
userName?: string
nickName?: string
email?: string
phonenumber?: string
}
// 提交时要求所有字段必填
type UserSubmitData = Required<UserForm>
const submitData: UserSubmitData = {
userName: 'admin', // 必填
nickName: '管理员', // 必填
email: 'admin@qq.com', // 必填
phonenumber: '13800138000' // 必填
}
// 配置验证函数
function validateConfig(config: Required<SystemConfig>) {
// 确保所有配置项都存在
console.log(config.theme)
console.log(config.language)
// 不会出现 undefined
}最佳实践:
- ✅ 用于数据提交前的类型约束
- ✅ 用于配置对象的完整性检查
- ❌ 避免与可选链(
?.)混用,容易产生困惑
Readonly - 所有属性只读
将类型 T 的所有属性转换为只读属性,防止修改。
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}使用场景:
- 不可变数据 - 防止意外修改
- 配置对象 - 运行时配置不应被修改
- 常量定义 - 定义常量对象
代码示例:
interface SystemConfig {
theme: string
language: string
pageSize: number
}
// 只读配置(运行时不允许修改)
const config: Readonly<SystemConfig> = {
theme: 'dark',
language: 'zh-CN',
pageSize: 20
}
// ❌ 编译错误: 无法分配到 "theme" ,因为它是只读属性
// config.theme = 'light'
// 深层只读
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P]
}
// 只读常量
const API_CONFIG: Readonly<{
BASE_URL: string
TIMEOUT: number
}> = {
BASE_URL: 'https://api.example.com',
TIMEOUT: 30000
}最佳实践:
- ✅ 用于配置对象,防止运行时修改
- ✅ 用于 Props 定义,确保不可变性
- ✅ 用于常量定义,明确表达不可修改意图
- ❌ 注意只读是浅层的,嵌套对象仍可修改
属性选择工具类型
这类工具类型用于从对象类型中选择或排除特定属性。
Pick - 选择部分属性
从类型 T 中选择指定的属性 K,构造新类型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}使用场景:
- 提取关键字段 - 只保留需要的字段
- 精简数据结构 - 减少传输数据量
- 视图模型 - 创建特定场景的数据模型
代码示例:
interface UserVo {
userId: string
userName: string
nickName: string
email: string
phonenumber: string
sex: string
avatar: string
status: string
createTime: string
updateTime: string
}
// 用户基本信息(只包含 id、用户名、昵称)
type UserBasic = Pick<UserVo, 'userId' | 'userName' | 'nickName'>
const userInfo: UserBasic = {
userId: '1',
userName: 'admin',
nickName: '管理员'
}
// 用户列表展示字段
type UserListItem = Pick<
UserVo,
'userId' | 'userName' | 'nickName' | 'status' | 'createTime'
>
// 用户选择器(下拉框)
type UserOption = Pick<UserVo, 'userId' | 'userName'>
const options: UserOption[] = [
{ userId: '1', userName: 'admin' },
{ userId: '2', userName: 'user' }
]最佳实践:
- ✅ 用于创建轻量级视图模型
- ✅ 用于 API 请求参数类型
- ✅ 用于下拉选择器选项类型
- ⚠️ 提取的字段应该有明确的业务含义
Omit - 排除部分属性
从类型 T 中排除指定的属性 K,保留其余属性。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>使用场景:
- 排除敏感字段 - 如密码、Token 等
- 表单数据 - 排除自动生成的字段(如 ID、时间戳)
- 数据脱敏 - 移除不需要展示的字段
代码示例:
interface UserBo {
userId: string
userName: string
password: string
nickName: string
email: string
phonenumber: string
createTime: string
updateTime: string
}
// 用户信息(排除密码)
type UserWithoutPassword = Omit<UserBo, 'password'>
const userInfo: UserWithoutPassword = {
userId: '1',
userName: 'admin',
nickName: '管理员',
email: 'admin@qq.com',
phonenumber: '13800138000',
createTime: '2024-01-01',
updateTime: '2024-01-02'
// password 字段不存在
}
// 新增用户表单(排除自动生成字段)
type UserAddForm = Omit<UserBo, 'userId' | 'createTime' | 'updateTime'>
const addForm: UserAddForm = {
userName: 'newuser',
password: '123456',
nickName: '新用户',
email: 'newuser@qq.com',
phonenumber: '13800138001'
}
// 排除多个字段
type UserPublicInfo = Omit<UserBo, 'password' | 'createTime' | 'updateTime'>最佳实践:
- ✅ 用于排除敏感信息
- ✅ 用于表单数据定义
- ✅ 用于 API 响应数据清洗
- ⚠️ 优先使用 Pick,Omit 作为补充
键值映射工具类型
这类工具类型用于创建键值对映射结构。
Record - 键值对类型
构造一个对象类型,键为 K,值为 T。
type Record<K extends keyof any, T> = {
[P in K]: T
}使用场景:
- 字典/映射 - 创建键值对映射
- 枚举映射 - 将枚举值映射到描述
- 配置对象 - 动态键名的配置
代码示例:
// 用户字典(ID -> 用户对象)
type UserMap = Record<string, UserVo>
const userMap: UserMap = {
'1': { userId: '1', userName: 'admin', /* ... */ },
'2': { userId: '2', userName: 'user', /* ... */ }
}
// 状态映射(状态码 -> 状态描述)
type StatusMap = Record<string, string>
const statusMap: StatusMap = {
'0': '正常',
'1': '停用'
}
// 权限映射
type PermissionMap = Record<string, boolean>
const permissions: PermissionMap = {
'system:user:add': true,
'system:user:edit': true,
'system:user:delete': false
}
// 表单错误映射
type FormErrors = Record<keyof UserForm, string | undefined>
const errors: FormErrors = {
userName: '用户名不能为空',
nickName: undefined,
email: '邮箱格式不正确'
}最佳实践:
- ✅ 用于创建映射表
- ✅ 用于枚举到描述的转换
- ✅ 用于缓存数据结构
- ⚠️ 注意键名类型约束
函数相关工具类型
这类工具类型用于提取函数的类型信息。
ReturnType - 函数返回类型
提取函数类型 T 的返回类型。
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any使用场景:
- API 返回类型 - 提取 API 函数返回类型
- 工具函数 - 基于函数推导类型
- 类型推导 - 避免重复定义
代码示例:
import { getUserInfo } from '@/api/system/user'
// 自动推导 API 返回类型
type UserInfo = ReturnType<typeof getUserInfo>
// 提取数据类型
type UserData = Awaited<ReturnType<typeof getUserInfo>>
// 工具函数返回类型
function formatUser(user: UserVo) {
return {
id: user.userId,
name: user.userName,
displayName: `${user.userName}(${user.nickName})`
}
}
type FormattedUser = ReturnType<typeof formatUser>
const user: FormattedUser = {
id: '1',
name: 'admin',
displayName: 'admin(管理员)'
}最佳实践:
- ✅ 用于推导 API 返回类型
- ✅ 用于工具函数类型提取
- ✅ 避免重复定义类型
- ⚠️ 函数需要明确的返回类型
Parameters - 函数参数类型
提取函数类型 T 的参数类型,返回元组类型。
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never使用场景:
- 参数复用 - 复用函数参数类型
- 高阶函数 - 包装函数时保持参数类型
- Mock 数据 - 生成测试数据
代码示例:
function updateUser(userId: string, data: Partial<UserVo>, options?: {
silent?: boolean
}) {
// 更新用户
}
// 提取参数类型
type UpdateUserParams = Parameters<typeof updateUser>
// [string, Partial<UserVo>, { silent?: boolean } | undefined]
// 使用参数类型
const params: UpdateUserParams = [
'1',
{ nickName: '新昵称' },
{ silent: true }
]
updateUser(...params)
// 提取单个参数类型
type UserId = Parameters<typeof updateUser>[0] // string
type UserData = Parameters<typeof updateUser>[1] // Partial<UserVo>
// 高阶函数包装
function withLoading<T extends (...args: any[]) => any>(fn: T) {
return (...args: Parameters<T>): ReturnType<T> => {
console.log('Loading...')
const result = fn(...args)
console.log('Done')
return result
}
}最佳实践:
- ✅ 用于函数参数类型复用
- ✅ 用于高阶函数类型定义
- ✅ 用于测试用例参数生成
- ⚠️ 元组类型访问需要索引
Awaited - Promise 返回类型
递归解包 Promise 类型,提取最终的值类型。
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T使用场景:
- 异步函数 - 提取 async 函数返回的实际类型
- Promise 链 - 解包嵌套 Promise
- API 调用 - 获取接口返回数据类型
代码示例:
// API 函数
async function getUserInfo(userId: string): Promise<R<UserVo>> {
return request.get(`/user/${userId}`)
}
// 提取 Promise 返回类型
type UserApiResponse = Awaited<ReturnType<typeof getUserInfo>> // R<UserVo>
// 提取数据类型
type UserData = Awaited<ReturnType<typeof getUserInfo>>['data'] // UserVo
// 嵌套 Promise
type NestedPromise = Promise<Promise<string>>
type Resolved = Awaited<NestedPromise> // string
// 异步处理函数
async function processUser(userId: string) {
const response = await getUserInfo(userId)
return response.data
}
type ProcessedUser = Awaited<ReturnType<typeof processUser>> // UserVo
// 实际使用
async function example() {
const user: Awaited<ReturnType<typeof getUserInfo>> = await getUserInfo('1')
console.log(user.data.userName)
}最佳实践:
- ✅ 用于异步函数返回类型提取
- ✅ 用于 Promise 链类型推导
- ✅ 结合 ReturnType 使用
- ⚠️ 注意 Promise 嵌套层级
项目自定义工具类型
Result - 统一 API 响应类型
统一的 API 请求返回格式,采用 [Error, Data] 元组结构。
declare type Result<T = any> = Promise<[Error | null, T | null]>设计理念:
受 Go 语言错误处理启发,将错误和数据封装在元组中,避免 try-catch,使错误处理更显式。
使用示例:
import type { UserVo } from '@/api/system/user/types'
// API 函数返回 Result 类型
async function getUserInfo(userId: string): Result<UserVo> {
try {
const response = await request.get(`/user/${userId}`)
return [null, response.data]
} catch (error) {
return [error as Error, null]
}
}
// 使用 Result 类型
async function handleGetUser() {
const [error, data] = await getUserInfo('1')
if (error) {
// 处理错误
ElMessage.error(error.message)
return
}
// 使用数据(TypeScript 知道 data 不为 null)
console.log(data.userName)
}优势:
- 显式错误处理 - 强制开发者处理错误
- 类型安全 - TypeScript 自动推导错误和数据的存在性
- 避免异常 - 不使用 try-catch,代码更简洁
- 统一风格 - 所有 API 调用采用一致的错误处理方式
R - 标准 API 响应结构
后端返回的标准响应格式,包含状态码、消息和数据。
declare interface R<T = any> {
code: number
msg: string
data: T
}使用示例:
// API 返回 R 结构
async function getUserList(): Promise<R<PageResult<UserVo>>> {
return request.get('/user/list')
}
// 处理响应
async function loadUsers() {
const response = await getUserList()
if (response.code === 200) {
const users = response.data.records
console.log(users)
} else {
ElMessage.error(response.msg)
}
}
// 泛型使用
type UserListResponse = R<PageResult<UserVo>>
type UserDetailResponse = R<UserVo>
type DeleteResponse = R<void>PageResult - 分页响应数据
标准化的分页数据结构,与后端 PageResult 保持一致。
declare interface PageResult<T = any> {
records: T[]
total: number
pages: number
current: number
size: number
last: boolean
}使用示例:
import type { UserVo } from '@/api/system/user/types'
// 分页响应
type UserPageResult = PageResult<UserVo>
const pageData: UserPageResult = {
records: [
{ userId: '1', userName: 'admin', /* ... */ },
{ userId: '2', userName: 'user', /* ... */ }
],
total: 100,
pages: 10,
current: 1,
size: 10,
last: false
}
// 分页查询函数
async function getUserPage(query: PageQuery): Promise<R<PageResult<UserVo>>> {
return request.get('/user/page', { params: query })
}
// 使用分页数据
async function loadPage() {
const response = await getUserPage({ pageNum: 1, pageSize: 20 })
const { records, total } = response.data
console.log(`共 ${total} 条记录`)
console.log(`当前页 ${records.length} 条`)
}PageQuery - 分页查询参数
用于后端分页查询的通用参数接口。
declare interface PageQuery {
pageNum?: number
pageSize?: number
orderByColumn?: string
isAsc?: string
searchValue?: string
params?: Record<string, any>
}使用示例:
// 基础分页查询
const query: PageQuery = {
pageNum: 1,
pageSize: 20
}
// 带排序的查询
const sortedQuery: PageQuery = {
pageNum: 1,
pageSize: 20,
orderByColumn: 'createTime',
isAsc: 'desc'
}
// 带搜索的查询
const searchQuery: PageQuery = {
pageNum: 1,
pageSize: 20,
searchValue: 'admin'
}
// 扩展查询参数
interface UserQuery extends PageQuery {
userName?: string
status?: string
deptId?: string
}
const userQuery: UserQuery = {
pageNum: 1,
pageSize: 20,
status: '0',
deptId: '100'
}DialogState - 弹窗状态配置
用于管理弹窗的显示状态和基本属性。
declare interface DialogState {
title?: string
visible: boolean
}使用示例:
import { reactive } from 'vue'
// 弹窗状态
const dialog = reactive<DialogState>({
title: '新增用户',
visible: false
})
// 打开弹窗
function openDialog(title: string) {
dialog.title = title
dialog.visible = true
}
// 关闭弹窗
function closeDialog() {
dialog.visible = false
}
// 多个弹窗
const addDialog = reactive<DialogState>({ title: '新增', visible: false })
const editDialog = reactive<DialogState>({ title: '编辑', visible: false })
const detailDialog = reactive<DialogState>({ title: '详情', visible: false })DictItem - 字典项配置
用于下拉选择、标签等组件的选项数据。
declare interface DictItem {
label: string
value: string
status?: string
elTagType?: ElTagType
elTagClass?: string
}使用示例:
// 状态字典
const statusDict: DictItem[] = [
{ label: '正常', value: '0', elTagType: 'success' },
{ label: '停用', value: '1', elTagType: 'danger' }
]
// 性别字典
const sexDict: DictItem[] = [
{ label: '男', value: '0' },
{ label: '女', value: '1' },
{ label: '未知', value: '2' }
]
// 在 Select 组件中使用
<el-select v-model="form.status">
<el-option
v-for="item in statusDict"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
// 字典标签
function getDictLabel(value: string, dict: DictItem[]): string {
return dict.find(item => item.value === value)?.label || '-'
}FieldConfig - 字段配置接口
用于详情展示、表单等组件的字段配置。
declare interface FieldConfig {
prop: string
label: string
span?: number
slot?: string
formatter?: (value: any, data: any) => string
type?: 'text' | 'copyable' | 'date' | 'datetime' | 'currency' |
'boolean' | 'array' | 'dict' | 'image' | 'password' | 'html' | 'file'
dictOptions?: DictItem[]
imageConfig?: {
width?: number | string
height?: number | string
showAll?: boolean
layout?: 'flex' | 'grid'
columns?: number
maxShow?: number
gap?: number
}
hidden?: boolean | ((data: any) => boolean)
group?: string
noPrint?: boolean
}使用示例:
// 用户详情字段配置
const userFields: FieldConfig[] = [
{ prop: 'userId', label: '用户ID', type: 'copyable' },
{ prop: 'userName', label: '用户名', type: 'text' },
{ prop: 'nickName', label: '昵称', type: 'text' },
{ prop: 'email', label: '邮箱', type: 'text' },
{
prop: 'sex',
label: '性别',
type: 'dict',
dictOptions: [
{ label: '男', value: '0' },
{ label: '女', value: '1' }
]
},
{
prop: 'avatar',
label: '头像',
type: 'image',
imageConfig: {
width: 100,
height: 100
}
},
{ prop: 'createTime', label: '创建时间', type: 'datetime' },
{ prop: 'updateTime', label: '更新时间', type: 'datetime' }
]SpanType / ResponsiveSpan - 响应式列数
支持固定数字、响应式对象或自动模式的栅格布局配置。
declare interface ResponsiveSpan {
xs?: number // <768px
sm?: number // ≥768px
md?: number // ≥992px
lg?: number // ≥1200px
xl?: number // ≥1920px
}
declare type SpanType = number | ResponsiveSpan | 'auto' | undefined使用示例:
// 固定列数
const fixedSpan: SpanType = 8 // 固定占 8 列
// 响应式列数
const responsiveSpan: SpanType = {
xs: 24, // 手机全宽
sm: 12, // 平板半宽
md: 8, // 桌面三分之一
lg: 6, // 大屏四分之一
xl: 4 // 超大屏六分之一
}
// 自动模式
const autoSpan: SpanType = 'auto'
// 在组件中使用
interface FormItemConfig {
prop: string
label: string
span?: SpanType
}
const formItems: FormItemConfig[] = [
{
prop: 'userName',
label: '用户名',
span: { xs: 24, md: 12 } // 手机全宽,桌面半宽
},
{
prop: 'nickName',
label: '昵称',
span: { xs: 24, md: 12 }
}
]FieldVisibilityConfig - 字段可见性配置
用于控制界面字段的显示/隐藏状态,支持层级结构。
declare interface FieldVisibilityConfig {
key: string | number
field: string
label: string
visible: boolean
children?: Array<FieldVisibilityConfig>
}使用示例:
// 表格列可见性配置
const columnVisibility: FieldVisibilityConfig[] = [
{
key: 'userId',
field: 'userId',
label: '用户ID',
visible: true
},
{
key: 'userName',
field: 'userName',
label: '用户名',
visible: true
},
{
key: 'nickName',
field: 'nickName',
label: '昵称',
visible: true
},
{
key: 'status',
field: 'status',
label: '状态',
visible: false
}
]
// 层级结构
const treeVisibility: FieldVisibilityConfig[] = [
{
key: 'basic',
field: 'basic',
label: '基本信息',
visible: true,
children: [
{ key: 'userName', field: 'userName', label: '用户名', visible: true },
{ key: 'nickName', field: 'nickName', label: '昵称', visible: true }
]
},
{
key: 'contact',
field: 'contact',
label: '联系方式',
visible: false,
children: [
{ key: 'email', field: 'email', label: '邮箱', visible: true },
{ key: 'phonenumber', field: 'phonenumber', label: '手机号', visible: true }
]
}
]高级使用技巧
类型组合
组合多个工具类型实现复杂类型转换。
// 部分字段必选,其他可选
type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>
type UserFormData = PartialExcept<UserVo, 'userName' | 'nickName'>
// 部分字段可选,其他必选
type RequiredExcept<T, K extends keyof T> = Required<T> & Partial<Pick<T, K>>
// 深层 Partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}条件类型
根据条件选择不同的类型。
// 提取对象类型的属性
type ObjectKeys<T> = {
[K in keyof T]: T[K] extends object ? K : never
}[keyof T]
// 提取函数类型的属性
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T]
// 非空类型
type NonNullable<T> = T extends null | undefined ? never : T映射类型
创建基于已有类型的新类型。
// 属性名添加前缀
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K]
}
type PrefixedUser = Prefixed<UserVo, 'user_'>
// { user_userId: string, user_userName: string, ... }
// 属性名转换为 Getter
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type UserGetters = Getters<UserVo>
// { getUserId: () => string, getUserName: () => string, ... }最佳实践
1. 优先使用内置工具类型
✅ 推荐:
type UserBasic = Pick<UserVo, 'userId' | 'userName' | 'nickName'>❌ 不推荐:
type UserBasic = {
userId: string
userName: string
nickName: string
}原因: 使用工具类型可以自动同步源类型的变化。
2. 合理使用类型推导
✅ 推荐:
type UserData = Awaited<ReturnType<typeof getUserInfo>>❌ 不推荐:
type UserData = R<UserVo> // 手动定义原因: 类型推导可以减少手动维护,自动保持一致性。
3. 避免过度使用 Partial
✅ 推荐:
interface UserUpdateData {
nickName?: string
email?: string
}❌ 不推荐:
type UserUpdateData = Partial<UserVo> // 过于宽松原因: Partial 会让所有字段可选,可能导致类型过于宽松。
4. 使用 Readonly 保护数据
✅ 推荐:
const config: Readonly<SystemConfig> = { /* ... */ }❌ 不推荐:
const config: SystemConfig = { /* ... */ }原因: Readonly 明确表达不可修改的意图,防止意外修改。
5. 组合工具类型实现复杂需求
✅ 推荐:
type UserUpdateForm = Omit<Required<Partial<UserVo>>, 'userId' | 'createTime'>原因: 组合工具类型可以实现复杂的类型转换。
常见问题
1. Pick 和 Omit 的选择
问题: 什么时候用 Pick,什么时候用 Omit?
解答:
- 字段较少时用 Pick - 明确指定需要的字段
- 字段较多时用 Omit - 排除少数不需要的字段
// ✅ 字段较少用 Pick
type UserBasic = Pick<UserVo, 'userId' | 'userName' | 'nickName'>
// ✅ 字段较多用 Omit
type UserWithoutSensitive = Omit<UserVo, 'password'>2. Partial 导致类型过于宽松
问题: 使用 Partial 后所有字段都可选,如何约束?
解答: 使用类型组合,部分字段必选。
// 问题: 所有字段都可选
type UserForm = Partial<UserVo>
// 解决: userName 和 nickName 必填,其他可选
type UserForm = Partial<UserVo> & Pick<UserVo, 'userName' | 'nickName'>
// 或使用自定义工具类型
type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>
type UserForm = PartialExcept<UserVo, 'userName' | 'nickName'>3. Record 键名类型约束
问题: Record 的键名类型如何约束?
解答:
// ❌ 键名过于宽松
type UserMap = Record<string, UserVo>
// ✅ 使用联合类型约束键名
type UserId = '1' | '2' | '3'
type UserMap = Record<UserId, UserVo>
// ✅ 使用模板字符串类型
type UserMap = Record<`user_${number}`, UserVo>4. Awaited 不解包自定义 Promise
问题: Awaited 无法解包自定义的类 Promise 对象?
解答: 确保对象实现了 PromiseLike 接口。
interface MyPromise<T> extends PromiseLike<T> {
then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): PromiseLike<TResult1 | TResult2>
}
type Result = Awaited<MyPromise<string>> // string5. 深层 Readonly 问题
问题: Readonly 只能让第一层属性只读,嵌套对象仍可修改?
解答: 使用 DeepReadonly 工具类型。
// 浅层只读
type ShallowReadonly = Readonly<UserVo>
// 深层只读
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P]
}
type FullReadonly = DeepReadonly<UserVo>类型调试技巧
1. 查看类型结果
// 使用 type 关键字查看推导结果
type Test = Pick<UserVo, 'userId' | 'userName'>
// 鼠标悬停可以看到结果
// 使用 Expand 工具类型展开
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
type TestExpanded = Expand<Test>2. 类型断言
// 断言类型正确
const user = {} as UserVo
// 使用 satisfies 检查类型
const config = {
theme: 'dark',
language: 'zh-CN'
} satisfies SystemConfig3. 条件类型调试
// 使用 Extract 和 Exclude 调试
type Test1 = Extract<'a' | 'b' | 'c', 'a' | 'b'> // 'a' | 'b'
type Test2 = Exclude<'a' | 'b' | 'c', 'a' | 'b'> // 'c'总结
工具类型是 TypeScript 类型系统的重要组成部分,掌握工具类型的使用可以显著提升开发效率和代码质量:
- 内置工具类型 - 熟练使用 Partial、Required、Pick、Omit、Record 等
- 项目类型 - 理解 Result、R、PageResult 等项目特定类型
- 类型组合 - 灵活组合工具类型实现复杂需求
- 类型推导 - 充分利用 TypeScript 的类型推导能力
- 最佳实践 - 遵循最佳实践,写出类型安全的代码
