【问题标题】:Variable number of generic arguments for React component in TypescriptTypescript 中 React 组件的可变数量的通用参数
【发布时间】:2021-10-18 14:13:04
【问题描述】:

在带有 TypeScript 的 ReactJS 中(在撰写本文时都是最新的稳定版本),我可以定义一个组件,当且仅当给定属性为真时,它才会有条件地呈现其 children。一种可能的实现可能是:

// I will be using this type helper in subsequent examples
export type Maybe<T> = T | undefined | null

export function Only<T>(props: {
    when: Maybe<T>
    children: ReactNode
}) {
    const {when, children} = props
    return <>{when ? children : null}</>
}

然后可以按以下方式使用它:

const MyComponent = (props: { value: number }) => (
    <div>Value: {props.value}</div>
);

export function MyPage(){
    const [value, setValue] = useState<number>()
    // value: number | undefined

    setTimeout(()=>{
        // Simulate a request
        setValue(42)
    }, 1000)

    return (
        <div>
            <Only when={value}>
                <MyComponent value={value!}/>
            </Only>
        </div>
    )
}

它的行为符合预期,但问题是我必须使用非空断言运算符! 才能将value 传递给MyComponent。幸运的是,我可以通过将缩小的类型传递给 children 来解决它:

export function Only<T>(props: {
    when: Maybe<T>
    children: ReactNode | ((when: T) => ReactNode)
}) {
    const {when, children} = props;
    return when ?
        <>{children instanceof Function ? children(when) : children}</>
        : null
}

然后我会以下列方式使用它:

<Only when={value}>{value => // Overshadowing with narrowed type
    <MyComponent value={value}/>
}</Only>

再一次,这按预期工作,尽管引入了一点冗长。 但是,如果我需要基于多个属性的“真实性”有条件地渲染一个组件并将它们传递给children,并缩小类型怎么办?一种明显的解决方案是嵌套上述Only 组件:

const MyComponent = (props: { value1: number, value2: string }) => (
    <>
        <div>Value1: {props.value1}</div>
        <div>Value2: {props.value2}</div>
    </>
);

export function MyPage() {
    const [value1, setValue1] = useState<number>()
    const [value2, setValue2] = useState<string>()

    setTimeout(() => {
        // Simulate a request
        setValue1(42)
        setValue2('foo')
    }, 1000)

    return (
        <div>
            <Only when={value1}>{value1 =>
                <Only when={value2}>{value2 =>
                    <MyComponent value1={value1} value2={value2}/>
                }</Only>
            }</Only>
        </div>
    )
}

尽管它可以处理任意数量的属性,但很容易看出这种方法是如何不可扩展的,因为我们需要编写大量样板代码才能以这种方式传递多个属性。理想情况下,我们将在数组中传递这些属性,其成员将以类似的方式缩小。例如,2 个属性的特殊情况如下所示:

export function Only2<T, S>(props: {
    when: [Maybe<T>, Maybe<S>]
    children: ReactNode | ((when: [T, S]) => ReactNode)
}) {
    const {when, children} = props
    return when[0] && when[1] ?
        <>{children instanceof Function ? children([when[0], when[1]]) : children}</>
        : null
}

然后它将以下列方式使用:

<Only2 when={[value1, value2]}>{([value1, value2]) =>
    <MyComponent value1={value1} value2={value2}/>
}</Only2>

不幸的是,我无法为一般情况提出解决方案,在这种情况下,我们能够以方便的方式传递和缩小任意数量的值的类型,如上面的 sn-p 所示。因此我的问题是双重的:是否有可能实现我在 TypeScript 中寻找的结果?如果没有,是否有任何替代策略来实现可以以方便的方式使用的通用组件,如使用Only 组件的 sn-ps 中所示?

【问题讨论】:

    标签: reactjs typescript typescript-generics


    【解决方案1】:

    你可以在这里使用元组。

    // Helper type to remove nulls from array
    type RemoveNulls<T> = {
        [K in keyof T]: NonNullable<T[K]>
    }
    
    // simplified example of a function
    // its generic parameter is a tuple
    function nonNullableArray<T extends readonly any[]>(items: T, fn: (items: RemoveNulls<T>) => void) {
        if (items.some(item => item === null)) {
            return;
        }
    
        // Unfortunately, we have to cast type here.
        // I can't think of any other way to narrow type of array by checking it's items
        fn(items as RemoveNulls<T>);
    }
    
    declare const a: string | null;
    declare const b: number | null;
    
    // `as const` is an important part. It helps typescript to narrow down type
    // from array to tuple
    // (string | number | null)[] vs [string | null, number | null]
    const v = nonNullableArray([a, b] as const, ([a, b]) => {
        console.log(a + b)
    })
    

    【讨论】:

      【解决方案2】:

      这是一种使用对象类型的方法,并说明了与 React 组件的使用。我假设您有一个不允许使用 null/undefined 的对象类型,并将其添加为选项。

      import React, {ReactNode} from 'react';
      
      type OrNull<T> = {
        [P in keyof T]: T[P] | undefined | null;
      };
      
      function Only<T>(props: {
          when: OrNull<T>,
          children: ReactNode | ((when: T) => ReactNode)
      }) {
          const {when, children} = props;
          for (const key in when) {
              if (when[key] == null) return null;
          }
          return <>
              {children instanceof Function ? children(when as T) : children}
          </>;
      }
      
      type FooBar = {
          foo: string,
          bar: boolean,
      }
      
      const child = ({foo, bar}: FooBar) =>
          <span>{foo} is {bar}</span>;
      
      <>
          <Only<FooBar> when={{foo: 'hi', bar: true}}>
              {child}
          </Only>
      
          <Only<FooBar> when={{foo: 'hi', bar: undefined}}>
              {child}
          </Only>
      </>
      

      Playground link

      【讨论】:

        【解决方案3】:

        感谢@edemaine 的精彩回答。在这个答案中,我只是将他的解决方案改编为我在问题中提供的示例,只是为了竞争,以防有人发现它有用:

        import React, {ReactNode, useState} from "react"
        
        type OrNull<T> = {
            [P in keyof T]: T[P] | undefined | null
        }
        
        export function Only<T>(props: {
            when: OrNull<T> | boolean // Allows usage of boolean literals
            children: ReactNode | ((when: T) => ReactNode)
        }) {
            const {when, children} = props
            if (typeof when === 'boolean') {
                if (!when)
                    return null
            } else {
                for (const key in when) {
                    if (!when[key])
                        return null
                }
            }
        
            return <>{children instanceof Function ? children(when as T) : children}</>
        }
        
        const MyComponent1 = (props: { value1: number }) => (
            <>
                <div>Value1: {props.value1}</div>
            </>
        )
        
        const MyComponent23 = (props: { value2: string, value3: boolean }) => (
            <>
                <div>Value2: {props.value2}</div>
                <div>Value3: {Boolean(props.value3).toString()}</div>
            </>
        )
        
        
        export function MyPage() {
            const [value1, setValue1] = useState<number>()
            const [value2, setValue2] = useState<string>()
            const [value3, setValue3] = useState<boolean>()
        
            setTimeout(() => {
                // Simulate a request
                setValue1(42)
                setValue2('foo')
                setValue3(true)
            }, 1000)
        
            return (
                <div>
                    <Only when={{value1, value2, value3}}>{({value1, value2, value3}) =>
                        <>
                            <MyComponent1 value1={value1}/>
                            <MyComponent23 value2={value2} value3={value3}/>
                        </>
                    }</Only>
                </div>
            )
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-01-19
          • 1970-01-01
          • 2016-11-19
          • 2021-07-28
          • 2021-06-17
          • 1970-01-01
          相关资源
          最近更新 更多