如何区分 React 上下文的联合

Posted

技术标签:

【中文标题】如何区分 React 上下文的联合【英文标题】:How to discriminate the union of React contexts 【发布时间】:2019-11-23 06:56:49 【问题描述】:

我有两种类型的 React 上下文,我需要能够确定它们的类型(移动和桌面)。如何以类型安全的方式执行此操作?

我尝试编写一个用户定义的类型保护,它利用以下属性:React.Context<LayoutContextType> 等同于React.Context<DesktopLayoutContextType> | React.Context<MobileLayoutContextType>

但是,我认为它们是等价的似乎是错误的。

interface ILayoutStateBase 
  nightMode: boolean


interface ILayoutContextBase<
  StateType extends ILayoutStateBase,
  Kind extends 'desktop' | 'mobile'
> 
  kind: Kind
  state: StateType


interface IDesktopState extends ILayoutStateBase 
  modalOpen: boolean


interface IMobileState extends ILayoutStateBase 
  sidebarOpen: boolean


type DesktopLayoutContextType = ILayoutContextBase<IDesktopState, 'desktop'>
type MobileLayoutContextType =  ILayoutContextBase<IMobileState, 'mobile'>

type LayoutContextType =
  | DesktopLayoutContextType
  | MobileLayoutContextType


// below results in:
/**
 * Type 'Context<ILayoutContextBase<IDesktopState, "desktop">>' 
 *   is not assignable to type 'Context<LayoutContextType>'.
 */
const isDesktopLayout = (
  ctx: React.Context<LayoutContextType>
): ctx is React.Context<DesktopLayoutContextType> => 
  return true // how can I do this?

我希望 TypeScript 能够识别 React.Context&lt;LayoutContextType&gt; 等同于 React.Context&lt;DesktopLayoutContextType&gt; | React.Context&lt;MobileLayoutContextType&gt; 并允许我使用 displayName 属性来区分它们。

但是,我在提供的代码中收到错误消息:

Type 'Context&lt;ILayoutContextBase&lt;IDesktopState, "desktop"&gt;&gt;' is not assignable to type 'Context&lt;LayoutContextType&gt;'.

【问题讨论】:

【参考方案1】:

抱歉,我的回答可能不完整,但可能会有所帮助。

    为什么下面的代码无法编译

    const isDesktopLayout = (
        ctx: React.Context<LayoutContextType>
    ): ctx is React.Context<DesktopLayoutContextType> => 
        return true // how can I do this?
    
    

    让我们看看React.Context&lt;T&gt; 类型。它有ProviderConsumer,它们都使用T 类型并将其用作props 的参数类型。 T 不在任何地方使用,除了作为参数类型。

    这会导致编译错误。要了解原因,请查看下面的简化示例。这是一个接受回调的函数。

    function f1 (callback: ((props: string | boolean) => void)) 
    

    尝试使用callback 调用f1 以下回调会出错

    f1((props: string) => );  // Type string | boolean is not assignable to type string
    

    当您尝试编写类型保护时,基本上会发生相同的情况。 React.Context&lt;LayoutContextType&gt;ProviderConsumer 定义为接受 props 类型为 LayoutContextType 的函数(进行了一些修改,但这并不重要)。并且您不能将上下文React.Context&lt;DesktopLayoutContextType&gt;ProviderConsumer 定义为接受props 类型DesktopLayoutContextType 的函数。

    幸运的是,通过使用上下文联合可以轻松解决此问题,如下所示

    const isDesktopLayout = (
        ctx: React.Context<DesktopLayoutContextType> | 
    React.Context<MobileLayoutContextType>
    ): ctx is React.Context<DesktopLayoutContextType> => 
        return true // how can I do this?
    
    

    第二个问题更难解决。类型保护在运行时用于查找ctx 的类型。但是React.Context&lt;DesktopLayoutContextType&gt;React.Context&lt;MobileLayoutContextType&gt; 之间的唯一区别是参数的类型。在运行时,无法找到回调函数想要接受的参数类型。由于 javascript 是动态语言,因此可以使用任意数量的任意类型的参数调用回调。而且我们无法提前知道参数的类型和数量。

    因此,在运行时区分上下文的一种方法可以是 diplayName 属性(如您所建议的那样)。它应该在上下文创建期间设置,之后不要更改,并在类型保护中检查。可以这样完成

    const isDesktopLayout = (
        ctx: React.Context<DesktopLayoutContextType> | 
        React.Context<MobileLayoutContextType>
    ): ctx is React.Context<DesktopLayoutContextType> => 
        if (ctx.displayName === undefined) throw 'context displayName is undefined!'
        return ctx.displayName === 'desktop'
    
    

    但是这个解决方案不绑定类型,而是绑定displayName 的值,可以在代码中的任何地方更改,这可能会导致错误。

【讨论】:

以上是关于如何区分 React 上下文的联合的主要内容,如果未能解决你的问题,请参考以下文章

如何区分antMatchers与路径变量与上下文路径的其余部分[重复]

NextJS + React 上下文:如何加载数据?

React - 如何获取上下文中钩子的值

React:如何使用相同类型的多个上下文,同时允许孩子从所有上下文中读取数据

React - 如何使用上下文从对象中应用多个值

如何使用 React 上下文 API?