隐藏导出的通用类型属性

huangapple go评论67阅读模式
英文:

Hide generic type prop on export

问题

以下是您要求的翻译部分:

I have a small set of components in which a Wrapper is going to manipulate its children (therefore referred to as Components by injecting a prop into each of the children via cloneElement.
我有一组小组件,其中一个 Wrapper 将通过 cloneElement 向其 children 注入一个 prop 来操作它们(因此将其称为 Component)。

The gotcha here is that Component props are of a generic type. When I expose Component on the code, I don't want one of its props to be on the signature because it will be automatically injected by the Wrapper component.
问题在于 Component 的 props 是一种通用类型。当我在代码中公开 Component 时,我不希望其中的一个 prop 出现在签名中,因为它将由 Wrapper 组件自动注入。

I have a concise example which shows what I mean:
我有一个简洁的示例,可以说明我的意思:

types.ts

export type SomeObject = {
  someKey: string;
};

type PropThatWillBeInjected<T extends SomeObject> = {
  fn: (value: string) => T;
};

export type WannaBePropTypes = {
  name: string;
};

export type PropTypes<T extends SomeObject> = PropThatWillBeInjected<T> &
  WannaBePropTypes;

重要提示:PropTypes<T>Component 预期的内容,但作为程序员,我希望 WannaBePropTypes 成为该组件的签名。
继续...

Component.tsx

function Component<T extends SomeObject>(props: PropTypes<T>) {
  const { fn, name } = props;
  const result = fn(name);
  return <div>Hello, {result.someKey}</div>
}

export default Component;

Wrapper.tsx

function Wrapper(props: { children: ReactNode }) {
  const { children } = props;
  return (
    <div id="wrapper">
      {React.Children.map(
        children as ReactElement<PropTypes<SomeObject>>,
        (child, index) =>
          cloneElement(child, {
            ...child.props,
            fn: (value: string) => ({
              someKey: `${value}-${index}`,
            }),
          })
      )}
    </div>
  );
}

export default Wrapper;

As expected, when I try to use these components as the following, the code works but the compiler complains:
如预期的那样,当我尝试像下面这样使用这些组件时,代码可以工作,但编译器会发出投诉:

<Wrapper>
  <Component name="Alice" />
  <Component name="Bob" />
</Wrapper>

Property 'fn' is missing in type '{ name: string; }' but required in type 'PropThatWillBeInjected<SomeObject>'.(2741)

Is there a way to cast Component so I don't need to pass fn manually? I know there's a way when the prop types is not generic...
有没有一种方法可以将 Component 转换,以便我不需要手动传递 fn?我知道当 prop 类型不是通用的时候有一种方法...

What I've tried:

  1. Making fn optional: works, but this is not the solution I'm looking for;
  2. Wrapping Component with another component and passing a noop to Component: works, but I don't want to create this unnecessary wrapper;
    我尝试过的方法:
  3. 使 fn 成为可选项:可以工作,但这不是我寻找的解决方案;
  4. 使用另一个组件包装 Component 并将 noop 传递给 Component:可以工作,但我不想创建这个不必要的包装器;

A playground with this sample code: StackBlitz(包含此示例代码的在线演示)。

英文:

I have a small set of components in which a Wrapper is going to manipulate its children (therefore referred to as Components by injecting a prop into each of the children via cloneElement.
The gotcha here is that Component props are of a generic type. When I expose Component on the code, I don't want one of its props to be on the signature, because it will be automatically injected by the Wrapper component.
I have a concise example which shows what I mean:

types.ts

export type SomeObject = {
  someKey: string;
};

type PropThatWillBeInjected&lt;T extends SomeObject&gt; = {
  fn: (value: string) =&gt; T;
};

export type WannaBePropTypes = {
  name: string;
};

export type PropTypes&lt;T extends SomeObject&gt; = PropThatWillBeInjected&lt;T&gt; &amp;
  WannaBePropTypes;

Important: PropTypes&lt;T&gt; is what Component expects, but as a programmer, I want WannaBePropTypes to be the signature of this component.
Moving on...

Component.tsx

function Component&lt;T extends SomeObject&gt;(props: PropTypes&lt;T&gt;) {
  const { fn, name } = props;
  const result = fn(name);
  return &lt;div&gt;Hello, {result.someKey}&lt;/div&gt;;
}

export default Component;

Wrapper.tsx

function Wrapper(props: { children: ReactNode }) {
  const { children } = props;
  return (
    &lt;div id=&quot;wrapper&quot;&gt;
      {React.Children.map(
        children as ReactElement&lt;PropTypes&lt;SomeObject&gt;&gt;,
        (child, index) =&gt;
          cloneElement(child, {
            ...child.props,
            fn: (value: string) =&gt; ({
              someKey: `${value}-${index}`,
            }),
          })
      )}
    &lt;/div&gt;
  );
}

export default Wrapper;

As expected, when I try to use these components as the following, the code works but the compiler complains:

&lt;Wrapper&gt;
  &lt;Component name=&quot;Alice&quot; /&gt;
  &lt;Component name=&quot;Bob&quot; /&gt;
&lt;/Wrapper&gt;

> Property 'fn' is missing in type '{ name: string; }' but required in type 'PropThatWillBeInjected<SomeObject>'.(2741)


Is there a way to cast Component so I don't need to pass fn manually? I know there's a way when the prop types is not generic...

What I've tried:

  1. Making fn optional: works, but this is not the solution I'm looking for;
  2. Wrapping Component with another component and passing a noop to Component: works, but I don't want to create this unnecessary wrapper;

A playground with this sample code: StackBlitz

答案1

得分: 1

如果我正确理解您的问题,您希望将 Component 作为 &lt;Component name=&quot;Alice&quot; /&gt; 进行调用,同时有一些内部逻辑来处理两种情况:fn 被传递和未被传递的情况。如果是这样,您可以创建不必要的类型(而不是不必要的包装),这些类型将是 WannaBePropTypes 或完整的 props 之一。这类似于您尝试的第一种和第二种的组合:

type FullProps&lt;T extends SomeObject&gt; = PropThatWillBeInjected&lt;T&gt; &amp; WannaBePropTypes;

type PropTypes&lt;T extends SomeObject&gt; = FullProps&lt;T&gt; | WannaBePropTypes;

因此,直到您在 Wrapper 组件中将 children 定义为 ReactElement&lt;FullProps&lt;SomeObject&gt;&gt;fn 是可选的。关于 Component,我认为您可以将 props 强制转换为 FullProps

const { fn, name } = props as FullProps&lt;T&gt;;

但如果出于某种原因您需要更严格的代码,可以通过以下方式缩小 props 类型:

function isFullProps&lt;T extends SomeObject&gt;(props: PropTypes&lt;T&gt;): props is FullProps&lt;T&gt; {
	return !!(props as FullProps&lt;T&gt;).fn;
}

function Component&lt;T extends SomeObject&gt;(props: PropTypes&lt;T&gt;) {
	if (!isFullProps(props)) return &lt;&gt;&lt;/&gt;;
	const { fn, name } = props;
	const result = fn(name);
	return &lt;div&gt;Hello, {result.someKey}&lt;/div&gt;;
}

尽管在您的情况下,这个条件似乎始终为 false。

这是如何只处理 TypeScript 的方法。

顺便说一下:也许您可以将 WannaBePropTypes 对象的数组传递给 Wrapper,而不是将 children 传递给它?如果 &lt;Component name=&quot;Alice&quot; /&gt; 不应该自己执行任何操作,这听起来更好。

英文:

If I inderstand your problem correctly, you want to call Component as &lt;Component name=&quot;Alice&quot; /&gt; and there should be some internal logic for two cases: when fn was passed and when not. If so, you can create unnecessary type (instead of unnecessary wrapper) which will be one of WannaBePropTypes or full props. This is like some combination of your try#1 and try#2:

type FullProps&lt;T extends SomeObject&gt; = PropThatWillBeInjected&lt;T&gt; &amp; WannaBePropTypes;

type PropTypes&lt;T extends SomeObject&gt; = FullProps&lt;T&gt; | WannaBePropTypes;

So fn is optional until you define children as ReactElement&lt;FullProps&lt;SomeObject&gt;&gt; in Wrapper component.
As for the Component, I think you can just cast props to FullProps:

const { fn, name } = props as FullProps&lt;T&gt;;

But if you need more strict code for some reason, you can narrow props type this way:

function isFullProps&lt;T extends SomeObject&gt;(props: PropTypes&lt;T&gt;): props is FullProps&lt;T&gt; {
	return !!(props as FullProps&lt;T&gt;).fn;
}

function Component&lt;T extends SomeObject&gt;(props: PropTypes&lt;T&gt;) {
	if (!isFullProps(props)) return &lt;&gt;&lt;/&gt;;
	const { fn, name } = props;
	const result = fn(name);
	return &lt;div&gt;Hello, {result.someKey}&lt;/div&gt;;
}

Though it seems this condition will always be false in your case.
This is how to tackle with Typescript only.

BTW: maybe you can just pass array of WannaBePropTypes objects into Wrapper instead of children? This sounds better if &lt;Component name=&quot;Alice&quot; /&gt; should do nothing by itself.

huangapple
  • 本文由 发表于 2023年2月18日 10:23:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/75490800.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定