如何在TypeScript中安全组合带Value属性的React Provider?
我想避免在应用组件外层嵌套大量Provider导致的「侧山状」组件层级问题,打算通过组合方式整合这些Provider。我的Provider大多带有value属性,已经找到了JS或用TSany类型的实现方式,但希望在不使用any的前提下,在TypeScript中安全组合带或不带value的Provider。
以下是我编写的示例代码,尝试组合两个带value的Provider,但遇到了类型错误:
import React from "react" import RefreshablePlayersContext from "contexts/RefreshablePlayersContext" import RefreshableGamesContext from "contexts/RefreshableGamesContext" import type { RefreshablePlayersContext as RefreshablePlayersContextType } from "types/RefreshablePlayersContext" import type { RefreshableGamesContext as RefreshableGamesContextType } from "types/RefreshableGamesContext" import type { Player } from "types/Player" import type { Game } from "types/Game" import type { RefreshableRequest } from "types/RefreshableRequest" interface ProviderWithValue<ContextType> { provider: React.Provider<ContextType> value: ContextType } interface ProvidersProps { children: React.ReactElement } const Providers = ({ children }: ProvidersProps): React.ReactElement => { const [refreshableGames, setRefreshableGames] = React.useState< RefreshableRequest<Game[]> >({ status: "Not Started", }) const [refreshablePlayers, setRefreshablePlayers] = React.useState< RefreshableRequest<Player[]> >({ status: "Not Started", }) const providersWithValues: [ ProviderWithValue<RefreshableGamesContextType>, ProviderWithValue<RefreshablePlayersContextType>, ] = [ { provider: RefreshableGamesContext.Provider, value: { refreshableGames, setRefreshableGames }, }, { provider: RefreshablePlayersContext.Provider, value: { refreshablePlayers, setRefreshablePlayers }, }, ] return providersWithValues.reduceRight((acc, CurrentProviderWithValue) => { return ( // TypeScript错误出在这里:传递给`value`属性的值不符合类型要求 <CurrentProviderWithValue.provider value={CurrentProviderWithValue.value}> {acc} </CurrentProviderWithValue.provider> ) }, children) } export default Providers
对应的TypeScript错误信息:
Type 'RefreshableGamesContext | RefreshablePlayersContext' is not assignable to type 'RefreshableGamesContext & RefreshablePlayersContext'.
Type 'RefreshableGamesContext' is not assignable to type 'RefreshableGamesContext & RefreshablePlayersContext'.
Type 'RefreshableGamesContext' is missing the following properties from type 'RefreshablePlayersContext': refreshablePlayers, setRefreshablePlayers
问题原因
TypeScript处理数组元素的联合类型时,会错误地将provider和value的类型做交叉推断,误以为所有元素的provider都需要接受两种value的交集类型,最终导致类型不匹配。
解决方案:泛型函数+类型区分实现安全组合
我们可以通过泛型函数为每个Provider单独推断类型,同时扩展支持无value的Provider:
1. 定义通用Provider类型
先区分带value和不带value的Provider类型:
// 带value的Provider类型 type ProviderWithValue<T> = { provider: React.Provider<T> value: T } // 不带value的Provider类型(仅需要children属性) type ProviderWithoutValue = { provider: React.ComponentType<{ children: React.ReactNode }> } & { value?: never } // 约束:不允许传入value属性 // 合并两种类型 type AnyProvider = ProviderWithValue<unknown> | ProviderWithoutValue
2. 创建泛型渲染函数
用泛型函数单独处理每个Provider的渲染,确保value和provider类型严格匹配:
const renderProvider = <T>( providerConfig: ProviderWithValue<T> | ProviderWithoutValue, children: React.ReactNode ) => { // 类型守卫:判断是否为带value的Provider if ('value' in providerConfig) { return <providerConfig.provider value={providerConfig.value}>{children}</providerConfig.provider> } return <providerConfig.provider>{children}</providerConfig.provider> }
3. 修改组合逻辑
在Providers组件中使用这个函数替代直接渲染:
const Providers = ({ children }: { children: React.ReactNode }): React.ReactElement => { const [refreshableGames, setRefreshableGames] = React.useState< RefreshableRequest<Game[]> >({ status: "Not Started", }) const [refreshablePlayers, setRefreshablePlayers] = React.useState< RefreshableRequest<Player[]> >({ status: "Not Started", }) const providers: AnyProvider[] = [ { provider: RefreshableGamesContext.Provider, value: { refreshableGames, setRefreshableGames }, }, { provider: RefreshablePlayersContext.Provider, value: { refreshablePlayers, setRefreshablePlayers }, }, // 示例:添加一个不带value的Provider(比如默认主题的ThemeProvider) { provider: ThemeProvider } ] return providers.reduceRight((acc, provider) => { return renderProvider(provider, acc) }, children) }
效果说明
- TypeScript会为每个Provider单独验证
value的类型是否匹配对应的provider,不会再出现联合类型交叉的错误 - 支持不带
value的Provider,且如果不小心给这类Provider传了value,TypeScript会直接抛出错误
内容的提问来源于stack exchange,提问作者pachun

