网站搜索

javascript - React 组件的自定义类型 - props 不匹配


我正在尝试创建一个函数,它接受两个参数:一个 React 函数组件及其 props 对象,并且有以下要求:

  • 该函数应该能够只接受一个参数,当没有组件所需的 props 时
  • 当组件的 props 缺失或与各自的类型不匹配时,Typescript 应该报错

`

import React, { PropsWithChildren, FC, FunctionComponent } from 'react';

function render<T> (
    Component: FC<PropsWithChildren<T>>,
    props = {} as PropsWithChildren<PropsWithChildren<T>>,
): JSX.Element {
    return <Component { ...props }>{ props?.children }</Component>;
}

interface FooProps { name: string; }

const Foo = ({ name }: FooProps): JSX.Element => <p>{name}</p>;
const Bar = (): JSX.Element => <p>bar</p>;


// Incorrect
render(Foo, {  name: 'asdd', a: '' }); // Passing. Expected to fail because prop 'a' is not expected
render(Foo); // Passing. Expected to fail because the component's prop 'name' is missing
render(Bar, { name: 'test' }); // Passing. Expected to fail, because Bar's props object is incorrect

// Correct
render(Foo, {}); // Failing. Expected to fail because prop 'name' is required
render(Foo, {  name: 'asdd' }); // Passing. Expected to pass with correct props
render(Bar); // Passing. Expected to pass because Bar does not require any props
render(Bar, {}); // Passing. Expected to pass because Bar does not require any props

`

使用当前版本的代码,我能够满足大部分要求,除了以下几点:

  • 当需要时,Typescript 应该要求 props 对象
  • Typescript 不应该允许传递组件不需要的 props

不确定我哪里做错了。任何建议都将不胜感激。

答案#1

我认为你对 PropsWithChildren 有一些误解。FC 已经在幕后使用了 PropsWithChildren,所以 FC<PropsWithChildren<T>> 是多余的。PropsWithChildren<PropsWithChildren<T>> 显然也是多余的。我认为你在那里使用两次,是为了修复因为将它添加到 FC 而导致的错误。

children 可以包含在展开运算符中。<Component { ...props }>{ props?.children }</Component> 与仅使用 <Component { ...props }/> 是一样的。

为了仅针对某些 T 的值使第二个参数可选,你需要将泛型与函数重载结合使用。

function render(Component: FC<{}>): JSX.Element
function render<T> (Component: FC<T>, props: PropsWithChildren<T>): JSX.Element
function render<T> (Component: FC<T>, props = {} as PropsWithChildren<T>): JSX.Element {
    return <Component { ...props }/>;
}

这不会因为额外属性而报错。我可以告诉你原因,但我不确定如何修复。当你调用 render(Foo, { name: 'asdd', a: '' }) 时,它会被推断为 render<{ name: string; a: string; }>。对于该类型,Foo 作为渲染组件是完全有效的。它被假设会忽略额外的属性。

答案#2

你正在使用 TypeScript 泛型,但你怎么期望 TS 会发现当你传入 Foo 作为第一个参数时,应使用 FooProps。你的解决方案只有在调用时指定模板参数才会有效。例如。

render<FooProps>(Foo, {  name: 'asdd', a: '' });

如果你把 FC 参数作为模板类,可能会更好。例如。

import React, { PropsWithChildren, FC, FunctionComponent } from 'react';

function render<T>(
    Component: (_props: T) => JSX.Element,
    props: PropsWithChildren<T> = {} as PropsWithChildren<T>,
): JSX.Element {
    return <Component {...props}>{props?.children}</Component>;
}

interface FooProps {
    name: string;
}

interface BarProps {
}

const Foo = ({ name }: FooProps): JSX.Element => <p>{name}</p>;
const Bar = ({}: BarProps): JSX.Element => <p>bar</p>;

这将解决你大部分的问题。然而,将默认值定义为空对象将不会在缺少参数时导致错误,因为你必须在定义时将其转换为正确的类型,但在未提供时它会忽略验证(因为已转换的默认值将被使用,所以永远不会失败)。