typescript React prop -与歧视性工会类型斗争

bjp0bcyl  于 2023-03-04  发布在  TypeScript
关注(0)|答案(5)|浏览(110)

我有两个具有相似 prop 的组件,但有一个关键的区别,一个名为TabsWithState的组件只包含一个 prop 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[]类型。此外,如果传递withRouterTabs还应接受可选的baseUrl属性。
我试着创建一个有区别的联合类型,如下所示:

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'.,正如在这个ts操场上看到的
所以我尝试输入TabsWithRouterTabsWithState作为接受TabsProps作为它们的 prop :

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中看到。
有趣的是,在这两种情况下,当我实际尝试 * 使用 * 组件时,这些 prop 都表现得很正确,这可以在ts playground底部的一些示例中看到。
我觉得我已经很接近了,但是我正在努力让这些有区别的联合类型按照我想要的方式运行,这样 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

这个ts练习场显示了captain-yossarian的解决方案没有强制我希望在Tabs组件上强制的类型

j8yoct9x

j8yoct9x1#

像这样简单的事情怎么样?根据需要使用as WithRouterPropsas WithStateProps给TypeScript一个提示。

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

ygya80vv2#

别名条件的控制流仅在ts@4.4之后可用。因此,使用 destructuredwithRouter变量在ts@4.4之前的if语句中使用时不携带任何类型信息。
然后在你的代码中有一个微妙的问题。你的withRouter prop的类型为boolean,如下所示:

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

您正在将其类型缩小为true,而不是booleantrue | 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,您可以使用描述的withRouter作为别名条件:

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

playground check

btxsgosb

btxsgosb3#

由于两个并集都有withROuter属性,因此TS很难区分它们。
我认为联盟值得重构。

UPDATE-添加了重载

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
顺便说一句,这两个问题可能对你很有意思。TS在与工会合作方面不太擅长重组

m1m5dgzv

m1m5dgzv4#

您可以定义单个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

brccelvz

brccelvz5#

在这种情况下,有两种方法可以缩小正确类型的范围

带有附加的tag属性

type WithStateProps = {
  tag: 'WithState'  
  withRouter?: boolean;
  baseUrl?: boolean;
  tabs: string[];
};

type WithRouterProps = {
  tag: 'WithRouter'  
  withRouter: boolean;
  baseUrl?: string;
  tabs: string[];
};

type TabsProps = WithStateProps | WithRouterProps

const Tabs = (props: TabsProps) => {
  if (props.tag === 'WithRouter') {
    return <TabsWithRouter {...props} />; // ✅ (parameter) props: WithRouterProps
  }
  return <TabsWithState {...props} />; // ✅ (parameter) props: WithStateProps
};

沙盒

使用类型 predicate

type WithStateProps = {
  withRouter?: boolean;
  baseUrl?: boolean;
  tabs: string[];
};

type WithRouterProps = {
  withRouter: boolean;
  baseUrl?: string;
  tabs: string[];
};

type TabsProps = WithStateProps | WithRouterProps

const Tabs = (props: TabsProps) => {
  if (isWithRouterProps(props)) {
    return <TabsWithRouter {...props} />; // ✅ (parameter) props: WithRouterProps
  }
  return <TabsWithState {...props} />; // ✅ (parameter) props: WithStateProps
};

function isWithRouterProps(props: WithStateProps | WithRouterProps): props is WithRouterProps {
  return (props as WithRouterProps).withRouter !== undefined;
}

沙盒
为您的特定情况扩展沙箱

相关问题