Evanss
Evanss

Reputation: 23153

Export TypeScript typed string prop values from a React component?

I have a React / TypeScript component. In 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>
    )
}

Which I can use in other components. In Page.tsx:

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

In Storybook I need to use all of the type values. In Button.stories.js:

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

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

Rather than having to repeat "primary", "secondary", "tertiary" is there a way I can export them from Button.tsx? That way if a new type is added the Storybook file will automatically have it.

I could use an enum in Button.tsx:

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

type Props = {
  type: Types;
};

However then components that use Button cant just pass a string, you would have to import the enum every time you used Button, which isn't worth the trade off. In Page.tsx:

import { Type } from './Button'

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

Upvotes: 2

Views: 3087

Answers (2)

danvk
danvk

Reputation: 16955

In TypeScript you can get a type from a value (using typeof) but you can never get a value from a type. So if you want to eliminate the duplication, you need to use a value as your source of truth and derive the type from it.

For example, if you make the array of button types the source of truth, then you can use a const assertion (as const) to derive the type from it:

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

Then you can import BUTTON_TYPES in your story and iterate over it.

You could also make a mapping from button type to color and use that as your source of truth. This would let you eliminate the color function from your component:

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

Upvotes: 1

Mosh Feu
Mosh Feu

Reputation: 29317

You can generate a declare an object and declare a type from it. This way, you'll be able to iterate through its keys and the type will be up to date on each change.

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 (I put the loop here but you can use it in the storybook of course)

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

Upvotes: 1

Related Questions