React 道具 - 努力区分联合类型
React props - struggling with discriminating union types
我有两个道具相似的组件,但有一个关键的区别。一个名为 TabsWithState
的组件仅包含一个道具 tabs
,它是以下形状的对象数组:
interface ItemWithState {
name: string;
active: boolean;
}
interface WithStateProps {
tabs: ItemWithState[];
};
另一个类似的组件,称为 TabsWithRouter
,要求项目形状不同:
interface ItemWithRouter {
name: string;
path: string;
}
interface WithRouterProps {
tabs: ItemWithRouter[];
};
我正在尝试创建一个通用组件 Tabs
,它可以解决这两种情况。我希望能够编写一个 <Tabs />
组件,如果传递 withRouter
道具,tabs
属性 必须是 ItemWithRouter[]
类型。但是如果没有传递 withRouter
属性,它必须是 ItemWithState[]
类型。此外,如果 withRouter
被传递,Tabs
也应该接受一个可选的 baseUrl
prop.
我尝试创建一个可区分的联合类型,如下所示:
type WithStateProps = {
withRouter?: never;
baseUrl?: never;
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: boolean;
baseUrl?: string;
tabs: ItemWithRouter[];
};
type TabsProps = WithStateProps | WithRouterProps;
在我的通用 Tabs
组件中,如果 withRouter
存在,我想渲染 TabsWithRouter
,如果 withRouter
不存在,我想渲染 TabsWithState
:
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
我最初尝试将 TabsWithRouter
和 TabsWithState
分别定义为接受 WithRouterProps
和 WithStateProps
的函数组件:
const TabsWithRouter: React.FC<WithRouterProps> = (props: WithRouterProps) => { ... }
const TabsWithState: React.FC<WithStateProps> = (props: WithStateProps) => { ... }
但我收到错误 Types of property 'withRouter' are incompatible. Type 'undefined' is not assignable to type 'boolean'.
,如 this ts playground
中所示
所以我尝试输入 TabsWithRouter
和 TabsWithState
作为接受 TabsProps
作为他们的道具:
const TabsWithRouter: React.FC<TabsProps> = (props: TabsProps) => {
const { tabs } = props;
console.log(tabs[0].path)
return null
}
const TabsWithState: React.FC<TabsProps> = (props: TabsProps) => {
const { tabs } = props;
console.log(tabs[0].active)
return null
}
但在这种情况下,尝试访问 tabs[x].path
或 tabs[x].active
时会出现错误 Property 'active' does not exist on type 'ItemWithState | ItemWithRouter'. Property 'active' does not exist on type 'ItemWithRouter'
,如 this ts playground.
中所示
有趣的是,在这两种情况下,当我实际尝试 使用 组件时,道具的行为是正确的,正如在任一 ts 游乐场底部的一些示例中所见.
我觉得我很接近,但我正在努力让这些有区别的联合类型以我想要的方式运行,以便 typescript 停止出错。我在这里阅读了很多询问类似问题的帖子,但我似乎无法将它们应用于我的场景中出现的问题。
编辑:
根据要求,这是我的 tsconfig.json:
{
"extends": "./tsconfig.paths.json",
"compilerOptions": {
"baseUrl": "src",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": [
"cypress",
"cypress-file-upload",
"jest"
],
"downlevelIteration": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
]
}
编辑2
this ts playground 表明 captain-yossarian 的解决方案没有强制执行我希望在 Tabs
组件
上强制执行的类型
像这样简单的事情怎么样...根据需要使用 as WithRouterProps
或 as WithStateProps
给 TypeScript 一个提示?
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter !== undefined) {
return <TabsWithRouter {...props as WithRouterProps} />;
}
return <TabsWithState {...props as WithStateProps} />;
};
您可以定义一个 TabsProps
类型:
type TabsProps = {
withRouter: boolean;
baseUrl?: string;
tabs: ItemWithRouter[] | ItemWithState[];
};
并在您的界面特定组件中使用它,例如:
const TabsWithRouter: React.FC<TabsProps> = (props: TabsProps) => {
const tabs = props.tabs as ItemWithRouter[];
return (
<div>
<h3>TabsWithRouter Path: {tabs[0].path}</h3>
</div>
);
};
const TabsWithState: React.FC<TabsProps> = (props: TabsProps) => {
const tabs = props.tabs as ItemWithState[];
return (
<div>
<h3>TabsWithState Path: {tabs[0].active ? "true" : "false"}</h3>
</div>
);
};
因为两个union都有withROuter
prop,所以TS很难区分
我认为 union 值得重构。
更新 - 添加重载
import React, { FC } from 'react'
interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
type TabsProps = WithStateProps | WithRouterProps;
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
const TabsWithRouter: FC<WithRouterProps> = (props: WithRouterProps) => null
const TabsWithState: FC<WithStateProps> = (props: WithStateProps) => null
type Overloading =
& ((props: WithStateProps) => JSX.Element)
& ((props: WithRouterProps) => JSX.Element)
const Tabs: Overloading = (props: TabsProps) => {
if (hasProperty(props, 'withRouter')) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With correct state props
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // WIth correct router props
withRouter
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth correct router props
withRouter
baseUrl="someurl"
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth incorrect router props
withRouter
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
现在,TS 可以计算出 withState
和 withRouter
在哪里
顺便说一句,这两个 and 问题对您来说可能都很有趣。
TS 在使用 union
方面不能很好地处理解构
aliased conditions 的控制流仅在 ts@4.4
后可用。因此,在 ts@4.4
.
之前的 if
语句中使用 destructured withRouter
变量不携带任何类型信息
那么你的代码中有一个微妙的问题。您的 withRouter
道具的类型为 boolean
,此处:
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter) { // withRouter === true
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
您正在将其类型缩小为 true
而不是 boolean
(true | false
)。
另一个问题是可选类型总是将 undefined
值带入联合。因此,您必须明确检查这种情况。此检查同时检查显式 'undefined' 值 或 !('withRouter' in props)
:
时的情况
const Tabs = (props: TabsProps) => {
if (props.withRouter === undefined) {
return <TabsWithState {...props} />;
}
return <TabsWithRouter {...props} />;
};
因为 ts@4.4
你可以使用 descstructured withRouter
作为别名条件:
const Tabs = (props: TabsProps) => {
const { withRouter } = props
if (withRouter === undefined) {
return <TabsWithState {...props} />;
}
return <TabsWithRouter {...props} />;
};
我有两个道具相似的组件,但有一个关键的区别。一个名为 TabsWithState
的组件仅包含一个道具 tabs
,它是以下形状的对象数组:
interface ItemWithState {
name: string;
active: boolean;
}
interface WithStateProps {
tabs: ItemWithState[];
};
另一个类似的组件,称为 TabsWithRouter
,要求项目形状不同:
interface ItemWithRouter {
name: string;
path: string;
}
interface WithRouterProps {
tabs: ItemWithRouter[];
};
我正在尝试创建一个通用组件 Tabs
,它可以解决这两种情况。我希望能够编写一个 <Tabs />
组件,如果传递 withRouter
道具,tabs
属性 必须是 ItemWithRouter[]
类型。但是如果没有传递 withRouter
属性,它必须是 ItemWithState[]
类型。此外,如果 withRouter
被传递,Tabs
也应该接受一个可选的 baseUrl
prop.
我尝试创建一个可区分的联合类型,如下所示:
type WithStateProps = {
withRouter?: never;
baseUrl?: never;
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: boolean;
baseUrl?: string;
tabs: ItemWithRouter[];
};
type TabsProps = WithStateProps | WithRouterProps;
在我的通用 Tabs
组件中,如果 withRouter
存在,我想渲染 TabsWithRouter
,如果 withRouter
不存在,我想渲染 TabsWithState
:
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
我最初尝试将 TabsWithRouter
和 TabsWithState
分别定义为接受 WithRouterProps
和 WithStateProps
的函数组件:
const TabsWithRouter: React.FC<WithRouterProps> = (props: WithRouterProps) => { ... }
const TabsWithState: React.FC<WithStateProps> = (props: WithStateProps) => { ... }
但我收到错误 Types of property 'withRouter' are incompatible. Type 'undefined' is not assignable to type 'boolean'.
,如 this ts playground
所以我尝试输入 TabsWithRouter
和 TabsWithState
作为接受 TabsProps
作为他们的道具:
const TabsWithRouter: React.FC<TabsProps> = (props: TabsProps) => {
const { tabs } = props;
console.log(tabs[0].path)
return null
}
const TabsWithState: React.FC<TabsProps> = (props: TabsProps) => {
const { tabs } = props;
console.log(tabs[0].active)
return null
}
但在这种情况下,尝试访问 tabs[x].path
或 tabs[x].active
时会出现错误 Property 'active' does not exist on type 'ItemWithState | ItemWithRouter'. Property 'active' does not exist on type 'ItemWithRouter'
,如 this ts playground.
有趣的是,在这两种情况下,当我实际尝试 使用 组件时,道具的行为是正确的,正如在任一 ts 游乐场底部的一些示例中所见.
我觉得我很接近,但我正在努力让这些有区别的联合类型以我想要的方式运行,以便 typescript 停止出错。我在这里阅读了很多询问类似问题的帖子,但我似乎无法将它们应用于我的场景中出现的问题。
编辑:
根据要求,这是我的 tsconfig.json:
{
"extends": "./tsconfig.paths.json",
"compilerOptions": {
"baseUrl": "src",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": [
"cypress",
"cypress-file-upload",
"jest"
],
"downlevelIteration": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
]
}
编辑2
this ts playground 表明 captain-yossarian 的解决方案没有强制执行我希望在 Tabs
组件
像这样简单的事情怎么样...根据需要使用 as WithRouterProps
或 as WithStateProps
给 TypeScript 一个提示?
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter !== undefined) {
return <TabsWithRouter {...props as WithRouterProps} />;
}
return <TabsWithState {...props as WithStateProps} />;
};
您可以定义一个 TabsProps
类型:
type TabsProps = {
withRouter: boolean;
baseUrl?: string;
tabs: ItemWithRouter[] | ItemWithState[];
};
并在您的界面特定组件中使用它,例如:
const TabsWithRouter: React.FC<TabsProps> = (props: TabsProps) => {
const tabs = props.tabs as ItemWithRouter[];
return (
<div>
<h3>TabsWithRouter Path: {tabs[0].path}</h3>
</div>
);
};
const TabsWithState: React.FC<TabsProps> = (props: TabsProps) => {
const tabs = props.tabs as ItemWithState[];
return (
<div>
<h3>TabsWithState Path: {tabs[0].active ? "true" : "false"}</h3>
</div>
);
};
因为两个union都有withROuter
prop,所以TS很难区分
我认为 union 值得重构。
更新 - 添加重载
import React, { FC } from 'react'
interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
type TabsProps = WithStateProps | WithRouterProps;
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
const TabsWithRouter: FC<WithRouterProps> = (props: WithRouterProps) => null
const TabsWithState: FC<WithStateProps> = (props: WithStateProps) => null
type Overloading =
& ((props: WithStateProps) => JSX.Element)
& ((props: WithRouterProps) => JSX.Element)
const Tabs: Overloading = (props: TabsProps) => {
if (hasProperty(props, 'withRouter')) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With correct state props
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // WIth correct router props
withRouter
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth correct router props
withRouter
baseUrl="someurl"
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth incorrect router props
withRouter
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
现在,TS 可以计算出 withState
和 withRouter
顺便说一句,这两个
aliased conditions 的控制流仅在 ts@4.4
后可用。因此,在 ts@4.4
.
if
语句中使用 destructured withRouter
变量不携带任何类型信息
那么你的代码中有一个微妙的问题。您的 withRouter
道具的类型为 boolean
,此处:
const Tabs = (props: TabsProps) => {
const { withRouter } = props;
if (withRouter) { // withRouter === true
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
您正在将其类型缩小为 true
而不是 boolean
(true | false
)。
另一个问题是可选类型总是将 undefined
值带入联合。因此,您必须明确检查这种情况。此检查同时检查显式 'undefined' 值 或 !('withRouter' in props)
:
const Tabs = (props: TabsProps) => {
if (props.withRouter === undefined) {
return <TabsWithState {...props} />;
}
return <TabsWithRouter {...props} />;
};
因为 ts@4.4
你可以使用 descstructured withRouter
作为别名条件:
const Tabs = (props: TabsProps) => {
const { withRouter } = props
if (withRouter === undefined) {
return <TabsWithState {...props} />;
}
return <TabsWithRouter {...props} />;
};