TypeScript不允许为React组件的props使用有效的联合类型。

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

TypeScript doesn't allow valid union type for React component props

问题

你的问题是关于TypeScript中联合类型和类型推断的问题。看起来你已经设计了联合类型,但TypeScript仍然无法正确地推断类型。在这种情况下,你可以尝试使用交叉类型(intersection types)来明确指定Props类型。

你可以将ParentComponentProps定义为RootParentComponentPropsComponentProps的交叉类型,确保所有的Props属性都被包含进来,而不依赖于TypeScript的类型推断。这样可以确保类型的准确性,避免了TypeScript的推断问题。

以下是修改后的代码:

type RootParentComponentProps = {
  label: string;
};

type ParentComponentProps = RootParentComponentProps & ComponentProps;

通过使用交叉类型,你明确地指定了ParentComponentProps的结构,避免了TypeScript无法正确推断的问题。希望这可以帮助你解决问题!

英文:

I've created a functional component that looks like this:

type RootComponentProps = {
  propThatAlwaysExists: string;
};

type ComponentPropsV1 = {
  type: "v1";
  onlyOnV1: string;
  onlyOnV2?: never;
};

type ComponentPropsV2 = {
  type: "v2";
  onlyOnV1?: never;
  onlyOnV2: string;
};

type ComponentProps = RootComponentProps & (
  | ComponentPropsV1
  | ComponentPropsV2
);

export const Component: FC<ComponentProps> = ({
  propThatAlwaysExists,
  type,
  onlyOnV1,
  onlyOnV2,
}) => {
  // do typechecked things, no errors in component body
};

The prop?: never type is a hack I'd learned to be allowed to destructure props, as TypeScript forbids direct access of any property not definitely guaranteed to exist, but destructuring is the preferred prop access method for code correctness tools like eslint, and in my opinion, code readability in general.

I'm then trying to mount this component in its parent, which passes most of these props through. I'd originally tried writing out the parent explicitly:

import { Component } from "./Component";

type RootParentComponentProps = {
  label: string;
  propThatAlwaysExists: string;
};

type ParentComponentPropsV1 = {
  type: "v1";
  onlyOnV1: string;
  onlyOnV2?: never;
};

type ParentComponentPropsV2 = {
  type: "v2";
  onlyOnV1?: never;
  onlyOnV2: string;
};

type ParentComponentProps = RootParentComponentProps & (
  | ParentComponentPropsV1
  | ParentComponentPropsV2
);

export const ParentComponent: FC<ParentComponentProps> = ({
  label,
  type,
  onlyOnV1,
  onlyOnV2,
}) => {
  // some effects, yadda yadda

  return (
    <div>
      <span>{label}</span>
      <Component
        propThatAlwaysExists={propThatAlwaysExists}
        type={type}
        onlyOnV1={onlyOnV1}
        onlyOnV2={onlyOnV2}
      />
    </div>
  );
};

But I got a very strange error:

Types of property 'onlyOnV1' are incompatible.
  Type 'string | undefined' is not assignable to type 'undefined'.
    Type 'string' is not assignable to type 'undefined'.ts(2322)

I thought this might be because TS was unable to be certain of the identical exclusions between the types, and realized I could DRY up my code some, so I changed ParentComponent's type definitions to this:

import { Component, ComponentProps } from "./Component";

type RootParentComponentProps = {
  label: string;
};

type ParentComponentProps = RootParentComponentProps & ComponentProps;

However, the error remained unchanged.
At this point, I thought the problem might be that TypeScript effectively loses the context of the exclusivity of the properties, and/or that the strictness of never really means never, so I've created an impossible arrangement of props by specifying both onlyOnV1 and onlyOnV2 when the union type I've created effectively says that only one of those properties can ever exist.
To combat this, I tried changing the exclusive types of Component from

type ComponentPropsV1 = {
  type: "v1";
  onlyOnV1: string;
  onlyOnV2?: never;
};

type ComponentPropsV2 = {
  type: "v2";
  onlyOnV1?: never;
  onlyOnV2: string;
};

to

type ComponentPropsV1 = {
  type: "v1";
  onlyOnV1: string;
  onlyOnV2: undefined;
};

type ComponentPropsV2 = {
  type: "v2";
  onlyOnV1: undefined;
  onlyOnV2: string;
};

but again the error remained unchanged, and this is where it stops making sense for me. As far as I can tell, these things are true:

  • TypeScript knows from the type of ComponentProps that onlyOnV1 and onlyOnV2 have inversely exclusive types, where if one is a string the other must be undefined and vice-versa
  • The type passed through from ParentComponent to Component is exactly the type of ComponentProps

Yet somehow, TypeScript has invented a scenario where the exclusive union type broadens from

{
  type: "v1";
  onlyOnV1: string;
  onlyOnV2: undefined;
} | {
  type: "v2";
  onlyOnV1: undefined;
  onlyOnV2: string;
}

to

{
  type: "v1" | "v2";
  onlyOnV1: string | undefined;
  onlyOnV2: string | undefined;
}

for the purposes of type checking against ComponentProps even though it literally is ComponentProps.

Am I using and/or designing my union types incorrectly?
Is there any way for me to make this work without sacrificing the explicitness of Component or ParentComponent?

答案1

得分: 2

Typescript 做了预期的操作;因为你解构了 props,type 可以是 v1v2;这就是你得到错误的原因。有两个选项: Option 1,不要解构 props 并传递整个对象: <Component {...props} /> Option 2,显式检查 type 的值: {type === 'v1' ? ( <Component propThatAlwaysExists={'propThatAlwaysExists'} type={type} onlyOnV1={onlyOnV1} onlyOnV2={onlyOnV2} /> ) : type === 'v2' ? ( <Component propThatAlwaysExists={'propThatAlwaysExists'} type={type} onlyOnV1={onlyOnV1} onlyOnV2={onlyOnV2} /> ) : null} playground

英文:

Typescript does what is expected; since you destruct the props, type can be v1 or v2; that's why you get the error. There are two options:
Option 1, don't destruct props and pass the whole object:

&lt;Component {...props} /&gt;

Option 2, check the value of type explicitly:

{type === &#39;v1&#39; ? (
        &lt;Component
          propThatAlwaysExists={&#39;propThatAlwaysExists&#39;}
          type={type}
          onlyOnV1={onlyOnV1}
          onlyOnV2={onlyOnV2}
        /&gt;
      ) : type === &#39;v2&#39; ? (
        &lt;Component
          propThatAlwaysExists={&#39;propThatAlwaysExists&#39;}
          type={type}
          onlyOnV1={onlyOnV1}
          onlyOnV2={onlyOnV2}
        /&gt;
      ) : null}

playground

答案2

得分: 0

After attempting wonderflame's answer without success, I realized that my "minimum reproducible example" was in fact oversimplified - not pictured here was an interstitial use of the TypeScript builtin Omit<T, U> type, which allows you to strike keys from an object.

Omit had been responsible for the type-broadening because it seems that composite types get combined together when simple object mapping types are run over them. To fix this, I had to be more explicit about my Component props as they're passed through from the parent, and spread the props intended for Component, changing this -

type ComponentPassthroughProps = Omit<ComponentProps, "strickenKey">

to this

type ComponentPassthroughProps = RootComponentProps & Omit<
  | ComponentPropsV1
  | ComponentPropsV2,
  "strickenKey"
>
英文:

After attempting wonderflame's answer without success, I realized that my "minimum reproducible example" was in fact oversimplified - not pictured here was an interstitial use of the TypeScript builtin Omit&lt;T, U&gt; type, which allows you to strike keys from an object.

Omit had been responsible for the type-broadening because it seems that composite types get combined together when simple object mapping types are run over them. To fix this, I had to be more explicit about my Component props as they're passed through from the parent, and spread the props intended for Component, changing this -

type ComponentPassthroughProps = Omit&lt;ComponentProps, &quot;strickenKey&quot;&gt;

to this

type ComponentPassthroughProps = RootComponentProps &amp; Omit&lt;
  | ComponentPropsV1
  | ComponentPropsV2,
  &quot;strickenKey&quot;
&gt;

huangapple
  • 本文由 发表于 2023年5月17日 12:25:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/76268553.html
匿名

发表评论

匿名网友

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

确定