TypeScript复杂泛型与组件。

3

我问了一个关于这个基础概念更加复杂版本的问题。
关联链接:可以在 TypeScript 中使用通用 JSX.Element 吗

我将其缩小到核心元素:

这是从 TypeA 获取参数的 Object A

type TypeA = {
  label: string
  value: number
}
const ObjA = ({ label, value }:TypeA) => {
  return <div>
    <div>Label: {label}</div>
    <div>Value: {value}</div>
  </div>
}

这是从TypeB获取参数的Object B

type TypeB = {
  label: string
  value: string
  bool: boolean
}
const ObjB = ({ label, value, bool }:TypeB) => {
  return <div>
    <div>Label: {label}</div>
    {bool && <div>Value: {value}</div>}
  </div>
}

现在我将这个 ComponentGroup 存储在一个数组中,并创建一个由该数组组成的 Type :
const ComponentCollection = [
  ObjA,
  ObjB
] as const
type Components = typeof ComponentCollection[number]

然后我创建一个通用组件:
interface GenericProps<T extends Components> {
  Component: T
  title: string
}
const Generic = <T extends Components,>({ Component, title, ...props }:GenericProps<T>) => {
  return (
    <div>
      <label>{title}</label>
      <Component {...props}/>
  </div>
  )
}

最后,我可以按以下方式调用通用组件:
<Generic Component={ObjA} title={'Usage A'}           label={'Object A'} value={'String A'}/>
<Generic Component={ObjB} title={'Usage B no Bool'}   label={'Object B'} value={0}/>
<Generic Component={ObjB} title={'Usage B with Bool'} label={'Object B'} value={0} bool/>

尽管它在JavaScript中运行得非常好,但我在输入方面出了些问题。
我设置了一个TS-Playground和一个Codepen:
TS-Playground:https://tsplay.dev/WvVarW
Codepen:https://codepen.io/Cascade8/pen/eYezGVV 目标:
  1. 将上述代码转换为正确的TypeScript代码
  2. 编译时没有任何TS错误或/@ts-ignore
  3. 使智能提示工作,因此如果您键入<Generic Component={ObjA} ...,它会显示此对象可用的类型属性。在这种情况下:label={string: } value={string: }
我不想要什么:
  1. 如果可能的话,我们的EsLint要求我们使用箭头函数,而不是类或旧的函数语法。
  2. 将对象作为子元素传递。
    我知道这样可以工作,但这并不是首选解决方案,因为主项目有很多像这样的组,会像这样呈现。
    为什么在JavaScript中非常简单的东西在TypeScript中不能工作呢。
2个回答

3

按照您构造类型的方式,您可以使用GenericProps中的泛型定义来执行此操作。

(注意,我已将props捆绑到一个新的props属性中,这样如果您有命名冲突就可以避免名称冲突)

import React from 'React'

type TypeA = {
  label: string
  value: number
}
const ObjA = ({ label, value }:TypeA) => {
  return <div>
    <label>{label}</label>
    <label>{value}</label>
  </div>
}
type TypeB = {
  label: string
  value: string
  bool: boolean
}
const ObjB = ({ label, value, bool }:TypeB) => {
  return <div>
    <label>{label}</label>
    {bool && <label>{value}</label>}
  </div>
}

type Components = typeof ObjA | typeof ObjB;

interface GenericProps<T extends (...args: any) => any> {
  Component: T
  title: string
  props: Parameters<T>[0]
}

const Generic = <T extends Components,>({ Component, title, props }:GenericProps<T>) => {
  return (
    <div>
      <label>{title}</label>
      <Component {...props as any}/>
    </div>
  )
}
const Usage = () => {
  return <Generic Component={ObjA} title={'Usage'} props={{label: 'ObjectA'}}/>
}
export default Generic

谢谢,这是最接近实现它的方法。我在TS Playground中重新创建了它:https://tsplay.dev/WyblKw 不幸的是,在组件部分仍然存在错误。 - Cascade_Ho
是的,你说得对。这是因为在Generic内部,props的类型没有指定为与Components中的条目匹配,而是在GenericProps内部完成的。不过,在使用它们的那一行中,你可以断言类型。你可以在里面直接转换为as any。这不会影响Generic内部的类型。 - Tom
@Cascade_Ho,以普通函数的方式调用组件,而不是通过JSX,即Component(props),而不是<Component {...props}/> 。我相信这将避免导致问题的LibraryManagedAttributes。 - sam256

1

完全可以不使用任何类型断言。 考虑以下示例:

import React, { FC, } from 'react'

type Type = {
  label: string;
  value: string | number
}

type TypeA = {
  label: string
  value: number
}

type FixSubtyping<T> = Omit<T, 'title'>

const ObjA = ({ label, value }: FixSubtyping<TypeA>) => {
  return <div>
    <label>{label}</label>
    <label>{value}</label>
  </div>
}

type TypeB = {
  label: string
  value: string
  bool: boolean
}

const ObjB = ({ label, value, bool }: FixSubtyping<TypeB>) => {
  return <div>
    <label>{label}</label>
    {bool && <label>{value}</label>}
  </div>
}


type Props<T> = T & {
  title: string
}


const withTitle = <T extends Type>(Component: FC<FixSubtyping<Props<T>>>) =>
  ({ title, ...props }: Props<T>) => (
    <div>
      <label>{title}</label>
      <Component {...props} />
    </div>
  )

const GenericA = withTitle(ObjA)
const GenericB = withTitle(ObjB)

const jsxA = <GenericA title={'Usage'} label={'A'} value={42} /> // // ok

const jsxB = <GenericB title={'Usage'} label={'A'} value={'str'} bool={true} /> // // ok

游乐场

此错误发生是因为 props 被推断为 Omit<T,'title'>,所以我们需要确保 TS 中的组件 propsOmit<T,'title'> 兼容。

然而,这样做会有一个缺点,您需要在其他组件中更新 props 类型。如果不是选项,我认为最好的方法是重载您的 Generic 函数:

import React, { FC, } from 'react'

type TypeA = {
  label: string
  value: number
}

type FixSubtyping<T> = Omit<T, 'title'>

const ObjA = ({ label, value }: TypeA) => {
  return <div>
    <label>{label}</label>
    <label>{value}</label>
  </div>
}

type TypeB = {
  label: string
  value: string
  bool: boolean
}

const ObjB = ({ label, value, bool }: TypeB) => {
  return <div>
    <label>{label}</label>
    {bool && <label>{value}</label>}
  </div>
}


type Props<T> = T & {
  Component: FC<T>,
  title: string
}


function Generic<T,>(props: Props<T>): JSX.Element
function Generic({ Component, title, ...props }: Props<unknown>) {
  return (
    <div>
      <label>{title}</label>
      <Component {...props} />
    </div>
  )
}

const jsxA = <Generic Component={ObjA} title={'Usage'} label={'A'} value={42} /> // // ok

const jsxB = <Generic Component={ObjB} title={'Usage'} label={'A'} value={'str'} bool={true} /> // // ok

Playground


谢谢你的解决方案。这个能用箭头函数来实现吗?我有点搞不定。 - Cascade_Ho
1
很抱歉,@Cascade_Ho,它应该推断出“T”泛型参数。 - captain-yossarian from Ukraine
好的。嗯,EsLint忽略 > @ts-ignore,我猜 :D - Cascade_Ho
我刚试图将这个适应到我的主要问题上,但现在组件没有类型限制。在底部解决方案中是否有可能限制它? - Cascade_Ho
1
@Cascade_Ho,您能否提供一个例子,让我明确您在类型限制方面遇到的问题? - captain-yossarian from Ukraine
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接