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} />;
};

我最初尝试将 TabsWithRouterTabsWithState 分别定义为接受 WithRouterPropsWithStateProps 的函数组件:

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

中所示

所以我尝试输入 TabsWithRouterTabsWithState 作为接受 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].pathtabs[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 WithRouterPropsas 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>
  );
};

https://codesandbox.io/s/vigilant-ptolemy-s2r3n

因为两个union都有withROuterprop,所以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 可以计算出 withStatewithRouter

在哪里

Playground

顺便说一句,这两个 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} />;
};

playground link

因为 ts@4.4 你可以使用 descstructured withRouter 作为别名条件:

const Tabs = (props: TabsProps) => {
  const { withRouter } = props
  if (withRouter === undefined) {
      return <TabsWithState {...props} />;
  }  
  
  return <TabsWithRouter {...props} />;
};

playground check