Exportera TypeScript-typade strängegenskapsvärden från en React-komponent?


Jag har en React/TypeScript-komponent. I Button.tsx:

type Props = {
  type: 
    | "primary"
    | "secondary"
    | "tertiary"
}

const Button = React.FC<Props> = ({ type }) => {
    const color = (() => {
        switch (type) {
          case "primary":
            return 'red';
          case "secondary":
            return 'blue';
          case "tertiary":
            return 'green';
          default:
            throw new Error("A backgroundColor condition was missed");
        }
    })();

    return(
        <button style={{ background: color }}>Im a button</button>
    )
}

Vilket jag kan använda i andra komponenter. I Page.tsx:

const Page = () => {
    return(
        <div>
            <h1>Heading</h1>
            <Button type="primary" />
        </div>
    )
}

I Storybook behöver jag använda alla typvärden. I Button.stories.js:

const types = [
  "primary",
  "secondary",
  "tertiary",
];

export const AllButtons = () => {
    return(
        types.map(type=>{
            <Button type={type} key={type} />
        })
    )
}

Istället för att behöva upprepa "primary", "secondary", "tertiary", finns det ett sätt jag kan exportera dem från Button.tsx? På det sättet, om en ny typ läggs till, kommer Storybook-filen automatiskt att ha den.

Jag skulle kunna använda en enum i Button.tsx:

export enum Types {
  primary = "primary",
  secondary = "secondary",
  tertiary = "tertiary",
}

type Props = {
  type: Types;
};

Men komponenter som använder Button kan inte bara skicka en sträng, du skulle behöva importera enum varje gång du använde Button, vilket inte är värt kompromissen. I Page.tsx:

import { Type } from './Button'

const Page = () => {
    return(
        <div>
            <h1>Heading</h1>
            <Button type={Type.primary} />
        </div>
    )
}

Svar #1

I TypeScript kan du få en typ från ett värde (genom att använda typeof) men du kan aldrig få ett värde från en typ. Så om du vill eliminera dupliceringen behöver du använda ett värde som din sanningskälla och härleda typen från det.

Till exempel, om du gör arrayen av knapptyper till sanningskällan, kan du sedan använda en const-assertion (as const) för att härleda typen från den:

// Button.tsx
export const BUTTON_TYPES = [
    "primary",
    "secondary",
    "tertiary",
] as const;

type Types = typeof BUTTON_TYPES[number];

type Props = {
    type: Types;
}

const Button: React.FC<Props> = ({ type }) => {
    // ...

    return(
        <button style={{ background: color }}>Im a button</button>
    )
}

Då kan du importera BUTTON_TYPES i din berättelse och iterera över det.

Du kan också göra en mappning från knapptyp till färg och använda det som din sanningskälla. Detta skulle låta dig ta bort color-funktionen från din komponent:

const TYPE_TO_COLOR = {
  primary: 'red',
  secondary: 'blue',
  tertiary: 'green',
} as const;

type Types = keyof typeof TYPE_TO_COLOR;
// type Types = "primary" | "secondary" | "tertiary"
export const BUTTON_TYPES = Object.keys(TYPE_TO_COLOR);

type Props = {
  type: Types;
}

const Button: React.FC<Props> = ({ type }) => {
  const color = TYPE_TO_COLOR[type];
  if (!color) {
    throw new Error("A backgroundColor condition was missed");
  }

  return (
    <button style={{ background: color }}>Im a button</button>
  );
}

Svar #2

Du kan skapa ett objekt och deklarera en typ från det. På detta sätt kommer du att kunna iterera genom dess nycklar och typen kommer att vara uppdaterad vid varje ändring.

Button.tsx

import * as React from "react";

export const ButtonSkins = {
  primary: "primary",
  secondary: "secondary",
  tertiary: "tertiary"
};

export type ButtonSkin = keyof typeof ButtonSkins;
export type ButtonProps = {
  skin: ButtonSkin;
};

export const Button: React.FC<ButtonProps> = ({ skin }) => (
  <button className={skin}>{skin}</button>
);

App.tsx (Jag lade loopen här men du kan naturligtvis använda den i storybook också)

import * as React from "react";
import { render } from "react-dom";
import { Button, ButtonSkins, ButtonSkin } from "./Button";

const App = () => (
  <div>
    {(Object.keys(ButtonSkins) as Array<ButtonSkin>).map(skin => {
      return <Button skin={skin} />;
    })}
    <h2>Start editing to see some magic happen {"\u2728"}</h2>
  </div>
);

render(<App />, document.getElementById("root"));

https://codesandbox.io/s/recursing-browser-qkgzb?file=/src/index.tsx