Reputation: 23153
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
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
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