ค้นหาเว็บไซต์

reactjs - React props - กำลังประสบปัญหากับการแยกประเภทเชิงสหภาพ


ฉันมีคอมโพเนนต์สองตัวที่มี props คล้ายกัน แต่มีความแตกต่างที่สำคัญ หนึ่งในคอมโพเนนต์ที่เรียกว่า 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[] นอกจากนี้ หากส่ง withRouter เข้ามา Tabs ก็ควรยอมรับพร็อพ baseUrl ที่เป็นทางเลือกได้ด้วย

ฉันพยายามสร้างชนิดยูเนียนแบบจำแนกประเภท แบบนี้:

type WithStateProps = {
  withRouter?: never;
  baseUrl?: never;
  tabs: ItemWithState[];
};

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

type TabsProps = WithStateProps | WithRouterProps;

ในคอมโพเนนต์ Tabs แบบทั่วไปของฉัน ฉันต้องการแสดง TabsWithRouter หากมี withRouter และแสดง TabsWithState หากไม่มี withRouter:

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'. ตามที่เห็นได้ใน ts playground นี้

ดังนั้นฉันจึงพยายามพิมพ์ TabsWithRouter และ TabsWithState เป็นการรับ TabsProps เป็น props ของพวกมัน:

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

ที่น่าสนใจคือ ในทั้งสองกรณี เมื่อฉันพยายามที่จะ ใช้ คอมโพเนนต์จริงๆ prop ก็ทำงานได้ถูกต้อง ซึ่งสามารถเห็นได้จากตัวอย่างบางส่วนที่ด้านล่างของ playground ts แต่ละอัน

ผมรู้สึกว่าผมใกล้จะได้แล้ว แต่ผมกำลังมีปัญหาในการทำให้ประเภท union ที่มีการแยกชนิดทำงานตามที่ผมต้องการเพื่อให้ 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 นี้แสดงให้เห็นว่าโซลูชันของกัปตัน-โยซซาเรียนไม่ได้บังคับใช้ประเภทที่ฉันต้องการบังคับใช้กับคอมโพเนนต์ Tabs

คำตอบ #1

จะเป็นอย่างไรกับสิ่งที่เรียบง่ายแบบนี้... การให้ TypeScript เบาะแสโดยใช้ as WithRouterProps หรือ as WithStateProps ตามความเหมาะสม?

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

คำตอบ #2

การควบคุมการไหลของเงื่อนไขที่มีนามแฝงสามารถใช้ได้ตั้งแต่ [email  เป็นต้นไป ดังนั้น การใช้ตัวแปร withRouter แบบ destructured จะไม่สามารถนำข้อมูลชนิดของตัวแปรมาใช้เมื่อถูกใช้ภายในคำสั่ง if ก่อน [email .

จากนั้นก็มีปัญหาเล็กน้อยในโค้ดของคุณ prop withRouter ของคุณมีประเภท boolean และที่นี่:

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

คุณกำลังจำกัดชนิดของมันเป็น true ไม่ใช่ boolean (true | false)

อีกปัญหาหนึ่งคือประเภทแบบเลือกได้จะนำค่่า undefined เข้าสู่ union เสมอ ดังนั้นคุณต้องตรวจสอบกรณีดังกล่าวอย่างชัดเจน การตรวจสอบนี้จะตรวจสอบทั้งค่าที่ระบุชัดเจนว่า 'undefined' หรือ กรณีที่ !('withRouter' in props):

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

ลิงก์สนามเด็กเล่น

ตั้งแต่ [email คุณสามารถใช้ withRouter แบบแยกโครงสร้างเป็นเงื่อนไขที่มีนามแฝงได้:

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

ตรวจสอบสนามเด็กเล่น

บทความที่เกี่ยวข้อง