英文:
TypeScript doesn't allow valid union type for React component props
问题
你的问题是关于TypeScript中联合类型和类型推断的问题。看起来你已经设计了联合类型,但TypeScript仍然无法正确地推断类型。在这种情况下,你可以尝试使用交叉类型(intersection types)来明确指定Props类型。
你可以将ParentComponentProps
定义为RootParentComponentProps
与ComponentProps
的交叉类型,确保所有的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
thatonlyOnV1
andonlyOnV2
have inversely exclusive types, where if one is astring
the other must beundefined
and vice-versa - The type passed through from
ParentComponent
toComponent
is exactly the type ofComponentProps
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
可以是 v1
或 v2
;这就是你得到错误的原因。有两个选项: 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:
<Component {...props} />
Option 2, check the value of type
explicitly:
{type === 'v1' ? (
<Component
propThatAlwaysExists={'propThatAlwaysExists'}
type={type}
onlyOnV1={onlyOnV1}
onlyOnV2={onlyOnV2}
/>
) : type === 'v2' ? (
<Component
propThatAlwaysExists={'propThatAlwaysExists'}
type={type}
onlyOnV1={onlyOnV1}
onlyOnV2={onlyOnV2}
/>
) : null}
答案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<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"
>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论