作为前端,你有没有过这些崩溃时刻?
- 写权限类型时重复敲
canRead、canWrite,手都酸了; - 处理联合类型时总担心访问不存在的属性;
- 写函数时因为输入输出类型不匹配 debug 半小时?
今天继续分享 5 个实用到哭的 TypeScript 技巧,帮你从“重复搬砖”升级到“智能编码”——每个都结合真实业务场景+完整示例代码,看完直接能用!
🔧 技巧 1:键重映射+值转换——让 TS 自动帮你写重复类型
谁懂啊!写 RBAC 权限系统时,要手动定义canRead、canWrite这种重复的布尔类型,简直像个“打字机”。更崩溃的是:后端改权限列表,前端要跟着改一堆类型!
真实业务场景:从后端权限到前端类型
假设后端返回的用户权限是一个字符串数组:["read", "write"],前端需要将其转成{ canRead: boolean; canWrite: boolean }的类型,用于渲染权限开关。
原来的“搬砖”写法(累!)
// 1. 手动定义权限类型(后端加权限,这里要跟着改)
type UserPermission = {
canRead: boolean
canWrite: boolean
canDelete: boolean // 后端新增delete权限,前端要手动加
}
// 2. 手动转换后端权限到前端类型
const convertPermission = (backendPerms: string[]) => {
return {
canRead: backendPerms.includes('read'),
canWrite: backendPerms.includes('write'),
canDelete: backendPerms.includes('delete'), // 又要手动加
}
}用键重映射+值转换的“智能”写法
核心思路:用 TS 的键重映射(as关键字)将后端的权限字符串(如read)自动转成前端的canRead,再用值转换统一设为布尔类型。
// 1. 定义后端返回的权限联合类型(后端改权限,只改这里!)
type BackendPerms = 'read' | 'write' | 'delete'
// 2. 自动生成前端权限类型(不用手动写canXXX!)
type FrontendPerms = {
// 🔑 键重映射:将BackendPerms的每个值转成canXXX(Capitalize首字母大写)
[K in BackendPerms as `can${Capitalize<K>}`]: boolean
}
// 3. 自动转换函数(后端权限数组→前端权限对象,不用手动加属性!)
const convertPerms = (perms: BackendPerms[]): FrontendPerms => {
// 用Object.fromEntries批量生成属性
return Object.fromEntries(
(Object.keys(BackendPerms) as BackendPerms[]).map((key) => [
`can${Capitalize(key)}`, // 转成canXXX
perms.includes(key), // 布尔值:是否包含该权限
])
) as FrontendPerms
}效果演示(绝了!)
// 后端返回的权限数组
const backendPerms = ['read', 'delete']
// 自动转换为前端权限对象
const userPerms = convertPerms(backendPerms)
// userPerms的类型是FrontendPerms:{ canRead: boolean; canWrite: boolean; canDelete: boolean }
// userPerms的值是:{ canRead: true, canWrite: false, canDelete: true }
// 渲染权限开关时,直接用类型约束的属性,不会错!
const readSwitch = (
<input type="checkbox" checked={userPerms.canRead} />
)
const writeSwitch = (
<input type="checkbox" checked={userPerms.canWrite} />
)为什么好用?
- ✅ 后端改权限,前端不用动类型:后端新增
"export"权限,只需在BackendPerms加"export",TS 自动生成canExport; - ✅ 避免拼写错误:
Capitalize自动处理首字母大写,不会写成canread或canWritee; - ✅ 覆盖更多场景:表单状态(如
canSubmit)、功能开关(如canUseAI)都能用这套逻辑!
🧙 技巧 2:类型守卫 with is——联合类型的“守门人”
处理多态数据时(比如购物车中的商品、用户和订单),直接访问属性很容易出错。比如:
type Product = { type: 'product'; price: number }
type User = { type: 'user'; email: string }
type Order = { type: 'order'; id: string } // 新增订单类型
type CartItem = Product | User | Order // 联合类型
const cart: CartItem[] = [
{ type: 'product', price: 99 },
{ type: 'user', email: 'foo@bar.com' },
{ type: 'order', id: '123' },
]
// ❌ 错误:CartItem可能没有price属性,TS报错!
cart.map((item) => item.price)用类型守卫“精准”收窄类型
核心思路:用is关键字定义类型守卫函数,告诉 TS“当函数返回 true 时,参数的类型是 XXX”。
// 1. 定义多个类型守卫(区分Product、User、Order)
const isProduct = (item: CartItem): item is Product => item.type === 'product'
const isUser = (item: CartItem): item is User => item.type === 'user'
const isOrder = (item: CartItem): item is Order => item.type === 'order'
// 2. 用类型守卫过滤数组,自动收窄类型!
const products = cart.filter(isProduct) // products的类型是Product[]
const users = cart.filter(isUser) // users的类型是User[]
const orders = cart.filter(isOrder) // orders的类型是Order[]
// ✅ 安全操作:products里的item一定有price属性!
const totalPrice = products.reduce((sum, p) => sum + p.price, 0) // 正确!
// ✅ 安全操作:orders里的item一定有id属性!
const orderIds = orders.map((o) => o.id) // 正确!更复杂的场景:混合操作
比如要计算购物车中商品的总价和订单的数量:
const calculateCartStats = (cart: CartItem[]) => {
let totalPrice = 0
let orderCount = 0
cart.forEach((item) => {
if (isProduct(item)) {
totalPrice += item.price // 🔍 类型收窄为Product,放心用price
} else if (isOrder(item)) {
orderCount += 1 // 🔍 类型收窄为Order,放心用id
}
})
return { totalPrice, orderCount }
}
// 调用函数,返回正确的统计结果
const stats = calculateCartStats(cart)
console.log(stats) // { totalPrice: 99, orderCount: 1 }为什么好用?
- ✅ 告别嵌套 if-else:用类型守卫把复杂的联合类型拆成清晰的分支;
- ✅ 完全避免类型错误:过滤后的数组类型明确,IDE 会提示对应的属性;
- ✅ 可复用:类型守卫函数可以在多个地方使用,不用重复写
item.type === "product"!
🔗 技巧 3:泛型工具类型+函数组合——类型安全的“流水线”
你有没有写过这样的表单数据处理逻辑:
- 去除用户输入的空格(trim);
- 将用户名转成大写(toUpper);
- 验证用户名长度(至少 3 位);
- 返回处理后的用户名和验证结果。
如果不用类型约束,很容易把number传给需要string的函数,或者返回错误的类型。
用泛型保持“流水线”的类型安全
核心思路:用泛型工具类型定义每个步骤的输入输出,再用函数组合将步骤连成流水线,确保每个步骤的类型匹配。
// 1. 定义每个步骤的函数(带类型约束)
const trim = (s: string): string => s.trim() // 去空格:string→string
const toUpper = (s: string): string => s.toUpperCase() // 转大写:string→string
const validateLength =
(min: number) =>
(s: string): { value: string; valid: boolean } => {
return { value: s, valid: s.length >= min } // 验证长度:string→{ value: string; valid: boolean }
}
// 2. 定义泛型管道类型(连接输入输出)
type Pipe<TIn, TOut> = (input: TIn) => TOut
// 3. 组合成类型安全的流水线(输入string→输出{ value: string; valid: boolean })
const processUsername: Pipe<string, ReturnType<typeof validateLength>> = (
input
) => {
const trimmed = trim(input) // 步骤1:去空格
const uppered = toUpper(trimmed) // 步骤2:转大写
const validated = validateLength(3)(uppered) // 步骤3:验证长度≥3
return validated
}真实业务场景:处理表单输入
// 模拟用户输入(可能有空格,小写)
const userInput = ' alice '
// 处理输入,得到类型安全的结果
const result = processUsername(userInput)
// result的类型是:{ value: string; valid: boolean }
// result的值是:{ value: "ALICE", valid: true }(长度5≥3)
// 根据验证结果提示用户
if (result.valid) {
console.log(`用户名${result.value}可用!`)
} else {
console.log('用户名长度必须≥3!')
}扩展:可变元组类型的动态流水线
如果需要更灵活的流水线(比如可添加任意步骤),可以用可变元组类型:
// 定义可变元组的管道类型(支持多个步骤)
type PipeTuple<T extends any[]> = T extends [infer First, ...infer Rest]
? Rest extends PipeTuple<Rest>
? (input: First) => LastOf<Rest>
: never
: never
// 辅助类型:取元组的最后一个类型
type LastOf<T extends any[]> = T extends [...infer _, infer Last] ? Last : never
// 组合多个函数成流水线
const pipe = <T extends any[]>(...fns: T): PipeTuple<T> => {
return (input: any) => fns.reduce((acc, fn) => fn(acc), input) as any
}
// 使用可变元组管道
const usernamePipeline = pipe(trim, toUpper, validateLength(3))
const result = usernamePipeline(' alice ') // 结果和之前一致!为什么好用?
- ✅ 强制类型匹配:如果
trim返回number,TS 会直接报错; - ✅ 促进代码复用:每个步骤的函数可以单独使用,也可以组合成流水线;
- ✅ 适合复杂逻辑:ETL 管道、数据标准化、表单处理都能用这套逻辑!
🧶 技巧 4:函数重载+类型收窄——让函数“读心”返回正确类型
你有没有写过根据输入返回不同类型的函数?比如:
- 输入
"json":返回object(解析 JSON); - 输入
"text":返回string(返回文本); - 输入
"binary":返回ArrayBuffer(返回二进制数据)。
如果不用函数重载,返回类型会是object | string | ArrayBuffer,调用时需要手动判断类型,很麻烦。
用函数重载“精准”定义返回类型
核心思路:用函数重载为每个输入类型定义对应的返回类型,让 TS 自动推断返回值的类型。
// 1. 定义函数重载(每个输入对应一个返回类型)
function parseResponse(type: 'json'): object // "json" → object
function parseResponse(type: 'text'): string // "text" → string
function parseResponse(type: 'binary'): ArrayBuffer // "binary" → ArrayBuffer
// 2. 实现函数(根据输入类型解析)
function parseResponse(
type: 'json' | 'text' | 'binary'
): object | string | ArrayBuffer {
switch (type) {
case 'json':
return JSON.parse('{"username": "alice", "age": 25}') // 返回object
case 'text':
return 'Hello, TypeScript!' // 返回string
case 'binary':
return new ArrayBuffer(8) // 返回ArrayBuffer
default:
throw new Error('无效的响应类型!')
}
}真实业务场景:解析 API 响应
// 模拟fetch API响应(带Content-Type)
const fetchData = async (url: string) => {
const response = await fetch(url)
const contentType = response.headers.get('Content-Type') || ''
// 根据Content-Type调用对应的解析函数
if (contentType.includes('application/json')) {
const data = parseResponse('json') // data的类型是object
console.log('JSON数据:', data.username) // ✅ 安全访问username
} else if (contentType.includes('text/plain')) {
const data = parseResponse('text') // data的类型是string
console.log('文本数据:', data.length) // ✅ 安全访问length
} else if (contentType.includes('application/octet-stream')) {
const data = parseResponse('binary') // data的类型是ArrayBuffer
console.log('二进制数据长度:', data.byteLength) // ✅ 安全访问byteLength
}
}
// 调用函数,自动解析不同类型的响应
fetchData('/api/user') // 返回JSON,解析为object
fetchData('/api/note') // 返回text,解析为string
fetchData('/api/file') // 返回binary,解析为ArrayBuffer为什么好用?
- ✅ 自动推断返回类型:输入
"json",TS 知道返回object,不用手动断言; - ✅ 避免类型错误:调用
parseResponse("json")后,放心访问object的属性; - ✅ 灵活扩展:新增响应类型(如
"csv"),只需加一个重载和对应的实现!
📐 技巧 5:元组解构+infer——提取函数参数的“神器”
写装饰器或中间件时,最烦的是“重复写函数参数类型”。比如写一个“权限校验装饰器”,需要检查用户是否有对应的权限,再执行原函数。如果原函数的参数变了,装饰器的参数也要跟着变,很容易错!
用 infer 自动提取函数参数
核心思路:用 TS 的infer 关键字提取原函数的参数元组,让装饰器的参数类型和原函数一致。
// 1. 定义泛型工具类型(提取函数参数)
type ExtractParams<T> = T extends (...args: infer A) => any ? A : never
// 解释:如果T是函数,infer A提取参数元组;否则返回never。
// 2. 定义权限校验装饰器(用infer提取原函数参数)
function requirePermission<T extends (...args: any[]) => any>(
permissions: string[]
) {
return (target: T) => {
// 🔑 用ExtractParams<T>提取原函数的参数类型
return (...args: ExtractParams<T>) => {
// 模拟权限校验(比如从localStorage取用户权限)
const userPerms = JSON.parse(
localStorage.getItem('userPerms') || '[]'
) as string[]
const hasPerm = permissions.every((perm) => userPerms.includes(perm))
if (hasPerm) {
return target(...args) // 有权限,执行原函数
} else {
throw new Error('没有权限执行此操作!')
}
}
}
}真实业务场景:装饰不同的函数
// 1. 原函数1:修改用户信息(需要"write"权限)
const updateUser = (id: string, data: { username: string }) => {
console.log(`修改用户${id}的信息:`, data)
}
// 2. 用装饰器添加权限校验(需要"write"权限)
const updateUserWithPerm = requirePermission(['write'])(updateUser)
// 3. 原函数2:获取用户信息(需要"read"权限)
const getUser = (id: string) => {
return { id, username: 'alice' }
}
// 4. 用装饰器添加权限校验(需要"read"权限)
const getUserWithPerm = requirePermission(['read'])(getUser)技巧 5 的完整示例:装饰器的调用与效果
我们用修改用户信息和删除用户的场景,展示装饰器的实际效果:
// 模拟用户权限(存在localStorage中,用户有"read"和"write"权限)
localStorage.setItem('userPerms', JSON.stringify(['read', 'write']))
// 1. 原函数:修改用户信息(需要"write"权限)
const updateUser = (id: string, data: { username: string }) => {
console.log(`修改用户${id}的信息:`, data)
}
// 用装饰器添加权限校验(需要"write"权限)
const updateUserWithPerm = requirePermission(['write'])(updateUser)
// 调用装饰后的函数(用户有"write"权限,执行成功)
updateUserWithPerm('123', { username: 'Alice' })
// 输出:修改用户123的信息:{ username: "Alice" }
// 2. 原函数:删除用户(需要"delete"权限)
const deleteUser = (id: string) => {
console.log(`删除用户${id}`)
}
// 用装饰器添加权限校验(需要"delete"权限)
const deleteUserWithPerm = requirePermission(['delete'])(deleteUser)
// 调用装饰后的函数(用户没有"delete"权限,抛出错误)
deleteUserWithPerm('123')
// 抛出Error:没有权限执行此操作!为什么 infer 这么好用?
假设原函数updateUser的参数新增了email字段,装饰器不需要任何修改,自动适配新参数:
// 原函数参数变化:新增email字段
const updateUser = (id: string, data: { username: string; email: string }) => {
console.log(`修改用户${id}的信息:`, data)
}
// 装饰器不用改!自动提取新的参数类型(id: string, data: { username: string; email: string })
const updateUserWithPerm = requirePermission(['write'])(updateUser)
// 调用时,参数类型和原函数完全一致(必须传email)
updateUserWithPerm('123', { username: 'Alice', email: 'alice@example.com' })
// 输出:修改用户123的信息:{ username: "Alice", email: "alice@example.com" }核心优势总结:
- ✅ 原函数参数变了,装饰器不用改:
ExtractParams<T>会自动更新参数类型; - ✅ 避免手动重复写参数:装饰器的参数和原函数 100%一致,不会漏传或错传;
- ✅ 适配所有函数:不管原函数有 1 个还是 10 个参数,装饰器都能自动处理!
🎯 最后:这些技巧到底能帮你什么?
这五个技巧不是“花活”,而是解决前端真实痛点的“利器”——
| 技巧 | 解决的痛点 |
|---|---|
| 键重映射+值转换 | 后端改权限,前端不用手动改类型 |
类型守卫 with is | 处理联合类型时,避免访问不存在的属性 |
| 泛型工具类型+函数组合 | 数据转换链中,保持类型安全 |
| 函数重载+类型收窄 | 一个函数处理多输入,自动推断返回类型 |
| 元组解构+infer | 写装饰器/中间件时,不用重复写参数类型 |
掌握它们,你的 TS 代码会发生质的变化:
- 高效:减少 80%的重复类型编写,把时间花在核心逻辑上;
- 安全:90%的类型错误在编译时被拦截,不用 debug 到深夜;
- 可扩展:需求变化时,只需改 1 处代码,其他地方自动适配。
📌 写在最后
TypeScript 的本质是“用类型描述业务逻辑”,而这些技巧的核心是“让类型自动适应业务变化”——从“手动写类型”到“让 TS 帮你写类型”,从“担心类型错误”到“让 TS 帮你检查错误”。
赶紧把这些技巧用到项目里吧!比如:
- 把权限类型换成键重映射,告别重复的
canXXX; - 给多类型数据加个类型守卫,避免访问不存在的属性;
- 用infer写个装饰器,简化权限校验或日志记录。
你会发现:原来 TS 不是“约束你的工具”,而是“懂你的伙伴”——它帮你减少重复,帮你避免错误,帮你更高效地写出可靠的代码。
互动时间:你用过哪个技巧?还有什么 TS 痛点?评论区聊聊~ 👇
- 本文链接:https://fridolph.top/posts/2025-05-28__ts04
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。