TypeScript如何基于嵌套键值实现联合类型的正确类型推断
双类型用户路由下的TS类型收窄实现方案
当前场景的核心问题是TS默认无法通过嵌套在role对象下的id字段自动完成联合类型的判别收窄,结合独立路由+React Context的架构,有3种可直接落地的方案,按改造成本从低到高排列:
方案1:自定义类型守卫,零改动适配现有数据结构
不需要修改API返回结构、不需要调整现有类型定义,通过TS的类型守卫函数,基于嵌套的role.id字段手动指定类型收窄规则即可。
实现代码
enum UserTypes { user = 'user', professionist = 'professionist' } // 客户端用户类型校验守卫 const isClient = (profile: UserProfile): profile is Client => { return profile.role.id === UserTypes.user } // 专业人士类型校验守卫 const isProfessionist = (profile: UserProfile): profile is Professionist => { return profile.role.id === UserTypes.professionist }
路由内使用方式
在对应类型的独立路由入口组件中,先通过守卫做权限校验,校验通过后TS会自动将变量推断为对应类型,可直接访问专属字段:
// 客户端专属路由组件 const ClientDashboard = () => { const { userProfile } = useUserContext() // 非客户端用户直接跳转无权限页 if (!isClient(userProfile)) return <Navigate to="/403" /> // 此处userProfile自动推断为Client类型,可直接访问subscriptions等客户端专属字段 console.log(userProfile.subscriptions) return <div>客户端工作台</div> }
专业人士路由同理,替换为isProfessionist守卫即可。
方案2:顶层添加判别字段,实现自动类型收窄
如果不想每次校验都导入守卫函数,可以在API数据存入Context之前做一层格式化,将判别字段提升到对象顶层,TS可基于顶层字段自动完成类型收窄,不需要额外写守卫逻辑。
实现代码
首先调整类型定义,给两类用户都添加顶层的userType判别字段:
type Client = { userType: UserTypes.user; // 顶层判别字段 role: ClientRole; id: string; first_name: string; last_name: string; display_name: string; email: string; related: RelatedUser[]; age: number; city: string; subscriptions: any[]; }; type Professionist = { userType: UserTypes.professionist; // 顶层判别字段 role: ProfessionistRole; id: string; first_name: string; last_name: string; display_name: string; email: string; related: RelatedUser[]; badges: any[]; tags: Tags[]; experiences: string[]; method: string; description: string; age: number; city: string; account: Account; plans: Plans[]; }; type UserProfile = Client | Professionist;
在API响应拦截器或者Context初始化的位置,对原始返回数据做格式化:
const formatUser = (rawUser: Omit<Client, 'userType'> | Omit<Professionist, 'userType'>): UserProfile => { return { ...rawUser, userType: rawUser.role.id } }
路由内使用方式
直接判断顶层userType字段即可,TS会自动收窄类型:
const ProfessionistDashboard = () => { const { userProfile } = useUserContext() if (userProfile?.userType !== UserTypes.professionist) return <Navigate to="/403" /> // 此处userProfile自动推断为Professionist类型,可直接访问plans、badges等专属字段 console.log(userProfile.plans) return <div>专业人士工作台</div> }
方案3:封装专属取值Hook,消除路由内重复校验
因为已经做了路由拆分,可以在Context层封装两类用户专属的取值Hook,把类型校验逻辑收敛到Hook内部,路由组件不需要重复写判断逻辑。
实现代码
基于方案1的类型守卫,封装两个专属Hook:
// 客户端路由专属取值Hook const useClientProfile = () => { const { userProfile } = useUserContext() if (!isClient(userProfile)) { // 配合路由错误边界做无权限跳转即可,也可以直接在这里做跳转处理 throw new Error("当前路由仅客户端用户可访问") } return userProfile } // 专业人士路由专属取值Hook const useProfessionistProfile = () => { const { userProfile } = useUserContext() if (!isProfessionist(userProfile)) { throw new Error("当前路由仅专业人士用户可访问") } return userProfile }
路由内使用方式
对应路由下直接调用对应Hook即可,拿到的直接是收窄好类型的数据:
// 客户端路由下的订单页组件 const ClientOrderPage = () => { const clientInfo = useClientProfile() // 直接访问Client类型的所有字段,不需要额外判断 return <div>当前用户:{clientInfo.display_name}</div> }
注意:如果采用抛错的校验方式,需要给路由组配置错误边界组件,捕获错误后跳转至对应提示页,避免页面白屏。
内容的提问来源于stack exchange,提问作者tmanuel
相关产品推荐
相关产品推荐

