当前位置: 首页 > news >正文

别再死记硬背了!用几个真实场景,带你吃透TypeScript的infer关键字

实战拆解:用TypeScript infer提升前端工程化能力

TypeScript的infer关键字就像一把瑞士军刀,它能帮我们在类型系统中"逆向工程"出隐藏的类型信息。很多开发者第一次接触infer时都会觉得它神秘莫测——这个在extends子句中突然出现的"魔法关键字"到底在做什么?今天我们就通过几个真实项目场景,看看infer如何解决实际开发痛点。

1. 从Promise链中提取最终返回值

现代前端开发中,异步操作无处不在。我们经常需要处理多层嵌套的Promise类型,特别是在处理API响应时。假设我们有一个用户信息接口,返回的是Promise<Promise<User>>这样的嵌套结构:

interface User { id: string name: string avatar: string } declare function fetchUser(): Promise<Promise<User>>

这时候如果直接用await fetchUser()确实能拿到User对象,但类型系统并不知道这一点。我们需要一个工具类型来提取最终的返回值类型:

type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T // 测试用例 type Test1 = UnwrapPromise<Promise<number>> // number type Test2 = UnwrapPromise<Promise<Promise<string>>> // string

这个递归解包的过程就像剥洋葱:

  1. 检查T是否是Promise类型
  2. 如果是,提取内部类型U
  3. 对U重复上述过程
  4. 直到不是Promise类型时返回T

在axios拦截器中,这个技巧特别有用:

// 统一处理API响应 axios.interceptors.response.use(response => { type ApiResponse<T> = { data: T code: number message: string } type RealData<T> = UnwrapPromise<T> extends ApiResponse<infer U> ? U : never const data: RealData<typeof response.data> = response.data.data return data })

2. 动态路由参数的类型安全

在Next.js或Nuxt.js项目中,我们经常需要处理动态路由参数。假设我们有一个博客详情页,路由格式是/posts/[id],如何确保从query中获取的id参数类型正确?

import { useRouter } from 'next/router' // 定义路由参数类型 type RouteParams<T extends string> = T extends `${string}[${infer P}]${infer Rest}` ? { [K in P | keyof RouteParams<Rest>]: string } : {} // 应用示例 type PostRoute = RouteParams<'/posts/[id]/comments/[commentId]'> // 得到 { id: string; commentId: string } function PostPage() { const router = useRouter() // 现在router.query会自动推断出正确的类型 const { id } = router.query as RouteParams<'/posts/[id]'> }

这个技巧同样适用于Vue Router:

const routes = [ { path: '/user/:userId/profile', component: UserProfile, props: (route) => ({ userId: route.params.userId // 自动推断为string }) } ]

更高级的用法是结合模板字面量类型:

type ExtractRouteParams<T> = T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractRouteParams<Rest>]: string } : T extends `${string}:${infer Param}` ? { [K in Param]: string } : {} type TestRoute = ExtractRouteParams<'/user/:userId/post/:postId'> // { userId: string; postId: string }

3. API响应类型的自动推断

在后端接口定义中,我们经常需要处理标准化的响应格式。假设所有API都遵循这样的结构:

interface ApiResponse<T> { success: boolean data: T error?: string }

我们可以创建一个类型工具来自动提取data的类型:

type ResponseData<T> = T extends ApiResponse<infer U> ? U : never // 用户相关API interface UserApis { getProfile: () => Promise<ApiResponse<User>> updateProfile: (data: Partial<User>) => Promise<ApiResponse<boolean>> } // 自动提取返回类型 type GetProfileResponse = ResponseData<Awaited<ReturnType<UserApis['getProfile']>>> // 等同于User type UpdateProfileResponse = ResponseData<Awaited<ReturnType<UserApis['updateProfile']>>> // 等同于boolean

在Pinia store中,这个技巧可以大幅简化类型定义:

import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { actions: { async fetchUser() { const res = await api.get('/user') // 返回ApiResponse<User> return res.data // 自动推断为User } } })

4. 组件Props的类型提取

在组件库开发中,我们经常需要基于现有组件派生出新组件。假设我们有一个基础按钮组件:

const Button = (props: { size: 'small' | 'medium' | 'large' variant: 'primary' | 'secondary' onClick: () => void }) => { /* 实现 */ }

现在要创建一个带图标的IconButton,需要复用Button的props类型:

type ExtractProps<T> = T extends (props: infer P) => any ? P : never type ButtonProps = ExtractProps<typeof Button> const IconButton = (props: ButtonProps & { icon: string iconPosition: 'left' | 'right' }) => { /* 实现 */ }

这个技巧在Vue的setup语法糖中特别有用:

<script setup> import BaseModal from './BaseModal.vue' // 提取基础组件的props类型 type ModalProps = InstanceType<typeof BaseModal>['$props'] defineProps<ModalProps & { customProp: string }>() </script>

5. 高阶函数类型推断

在函数式编程中,我们经常需要处理高阶函数的类型。比如实现一个memoize函数:

function memoize<T extends (...args: any[]) => any>(fn: T): T { const cache = new Map() return function(...args: Parameters<T>): ReturnType<T> { const key = JSON.stringify(args) if (cache.has(key)) return cache.get(key) const result = fn(...args) cache.set(key, result) return result } as T } // 使用示例 const add = (a: number, b: number) => a + b const memoizedAdd = memoize(add)

这里用到了Parameters和ReturnType这两个内置工具类型,它们的实现其实也用到了infer:

type Parameters<T> = T extends (...args: infer P) => any ? P : never type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any

在React中,我们可以用这个技巧创建类型安全的高阶组件:

function withLoading<T extends React.ComponentType<any>>( Component: T ): React.FC<React.ComponentProps<T> & { isLoading?: boolean }> { return ({ isLoading, ...props }) => { return isLoading ? <Spinner /> : <Component {...props} /> } } // 使用示例 const UserCard = (props: { user: User }) => <div>{props.user.name}</div> const UserCardWithLoading = withLoading(UserCard) // 现在UserCardWithLoading的props类型自动包含user和isLoading

6. 类型安全的Redux reducer

在Redux状态管理中,我们可以利用infer创建类型安全的action分发:

type Action<T extends string, P> = { type: T payload: P } type ActionCreator<T extends string, P> = (payload: P) => Action<T, P> function createAction<T extends string, P>( type: T ): ActionCreator<T, P> { return (payload: P) => ({ type, payload }) } // 自动推断action类型 type InferAction<T> = T extends ActionCreator<infer Type, infer Payload> ? Action<Type, Payload> : never const login = createAction('user/login')<{ username: string; password: string }>() type LoginAction = InferAction<typeof login> /* { type: 'user/login' payload: { username: string; password: string } } */

在Pinia中,类似的模式可以这样实现:

import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { actions: { increment(amount: number) { this.count += amount } } }) // 提取action类型 type StoreActions<T> = T extends { actions: infer A } ? A : never type CounterActions = StoreActions<typeof useCounterStore> /* { increment: (amount: number) => void } */

7. 条件类型与分布式条件类型

infer与条件类型结合使用时,会产生分布式条件类型的特性。这在处理联合类型时特别有用:

type ExtractArrayType<T> = T extends (infer U)[] ? U : T type Test1 = ExtractArrayType<string[]> // string type Test2 = ExtractArrayType<(string | number)[]> // string | number

在处理表单验证时,这个技巧可以帮助我们创建灵活的类型:

type ValidationResult<T> = { [K in keyof T]: T[K] extends any[] ? string[] | undefined : string | undefined } interface UserForm { name: string age: number hobbies: string[] } type UserFormErrors = ValidationResult<UserForm> /* { name?: string age?: string hobbies?: string[] } */

在React中,我们可以用这个模式创建类型安全的表单组件:

type FormFieldProps<T> = { name: keyof T value: T[keyof T] onChange: (name: keyof T, value: T[keyof T]) => void error?: ValidationResult<T>[keyof T] } function FormField<T>(props: FormFieldProps<T>) { /* 实现 */ } // 使用示例 <FormField<UserForm> name="hobbies" value={[]} onChange={(name, value) => console.log(name, value)} error={['Hobby is required']} />
http://www.jsqmd.com/news/667478/

相关文章:

  • Bilibili视频批量下载工具:5分钟快速上手,高效管理你的B站资源库
  • 2026 无锡防水补漏 4 家优质服务商推荐,地下室厨房高效止漏 - 十大品牌榜单
  • Creo二次开发实战:如何用ProModeCurrentGet函数精准判断当前打开的是零件还是装配体?
  • 【GStreamer实战】从USB相机到文件:一站式掌握图片抓取与视频录制
  • 告别手动点点点:用Python+pywin32脚本化你的CANoe自动化测试(附完整代码)
  • 立创EDA实战指南:从零到一打造STM32核心板
  • 别再傻傻用locateCenterOnScreen了!实测PyAutoGui图像定位,这个组合速度更快
  • 单车共享单车已标注数据集分享(适用于YOLO系列深度学习分类检测任务)
  • LaTeX三线表进阶:从基础横竖线到自定义短横线的精细排版
  • C# Winform Chart控件进阶:多图表联动与实时数据流可视化
  • QT+OpenCV项目实战:给你的视觉软件装上‘快搜’引擎,基于NCC的模板匹配保姆级集成教程
  • OrthoFinder结果深度挖掘:从Orthogroup到功能注释与进化分析的完整流程
  • OpenCV C++实战:cvtColor()色彩空间转换核心用法与场景解析
  • 别再让日志撑爆硬盘了!Spring Boot项目里Logback的maxHistory和totalSizeCap到底怎么配?
  • 【VC7升级VC8实战】从规划到验证:vCenter Server 8.0 无缝升级全流程拆解
  • 浪潮NF5280M5服务器装ESXi 6.7,手把手教你搞定PM8060 RAID卡驱动缺失问题
  • C# 15 类型系统改进:Union Types
  • TLK2711芯片的8B/10B编码与Comma发送详解:从原理到FPGA代码实现(附Verilog示例)
  • 别再一张张画ROC曲线了!用Python的sklearn和matplotlib,5分钟搞定多模型性能对比图
  • 交通大脑≠AI堆砌!AGI城市管理系统必须满足的5项硬性合规条款(源自《GB/T 43722-2024 智能城市AGI应用安全规范》)
  • 告别数据丢失!用F460的PVD2功能做个掉电预警,手把手教你保存关键参数
  • CloudCompare——点云最小包围盒的PCA算法原理与实战解析【2025】
  • 专业PCB逆向分析利器:OpenBoardView深度实战指南
  • C# Winform Chart控件进阶:打造专业级交互式饼状图
  • 5分钟掌握Windows网络测速神器:iperf3-win-builds完全指南
  • ESP系列芯片上电瞬间:GPIO默认状态解析与电路设计避坑指南
  • 在‘内网’搞AI?我用Conda+mamba+阿里云源搭Python环境的完整记录
  • PyMuPDF进阶:精准定位与智能替换PDF文本的实战指南
  • AGI能否出具无保留意见审计报告?:2025年AICPA新规倒计时47天,3类不可自动化判断事项必须人工复核
  • 你的J-Link-OB驱动装对了吗?从驱动安装到MDK5/Keil配置的完整避坑流程