Safer string props using Typescript's template literals
The Problem
String based props have no constraint. As long as it is a string it can be any string, but there are cases where you don't want just any string to be passed as a prop to your React component. You need safety built in so that you or your team doesn't shoot yourself in the foot later.
Here are ways to make safer props for your components
There are many patterns that you are probably familiar with for solving this issue of:
make impossible states impossible.
Booleans
We have all seen this one. Our component has two versions. A normal version and a version with an extra illustration. So, we have a isFancy
prop that allows us to explicitly turn it off and on.
Enums
Uh oh, we can't use a boolean because there are more than two options, but a string is too generic. Enter the enum
we can now say there are these 4 specific button types:
type Variant = "primary" | "secondary" | "ghost" | "outline"
// or actually use an enum
enum Variant {
Primary,
Secondary,
Ghost,
Outline
}
Then in our button component we can match on which variant was passed in and do whatever it is that we need.
Enums + Config
Okay, now we are really getting some complexity with out components. Our button needs to change more than one element depending on the variant. So, we can use what I term an enumerated object prop. Let's take a look at how that works:
const variants = {
primary: {
bg: "from-gray-100 to-gray-100 group-hover:from-accent group-hover:to-accent-bright group-focus:from-accent group-focus:to-accent-bright",
text: "group-hover:text-white group-focus:text-white",
},
danger: {
bg: "from-gray-100 to-gray-100 group-hover:from-danger group-hover:to-danger-hover group-focus:from-danger group-focus:to-danger-hover",
text: "group-hover:text-white group-focus:text-white",
},
light: {
bg: "from-white/25 to-white/10 group-hover:from-accent group-hover:to-accent-dark group-focus:from-accent-dark group-focus:to-accent",
text: "",
},
} as const;
// This is the type you can now use as a prop for your component.
// It will only allow you to pass string that match the keys in your variants config object.
type Variant = {
variant?: keyof typeof variants;
};
All of these options are great! What do you do about cases where there is a pattern to the string argument, but there are so many potential options it would be unreasonable to explicitly create types for every possible combo? Typescript actually provides just what we need.
Let's talk about template literals
Typescript's template literals are the perfect tool for constraining strings that follow a specific pattern, but have more possible variants than is reasonable to explicitly create individual types for using the patterns mentioned above.
Template literals in typescript work very similar to their non-type counterpart. Instead of passing in variables to fill holes in a string pattern we pass a type. Let's look at an example really quick before diving into a full use case:
type Literal = `pattern-${string}`
// Matches
'pattern-one'
'pattern-photograph'
'pattern-this-still-matches'
// Doesn't match
'patt-one'
'patternphoto'
'obviously-not-pattern'
Okay, so how is this useful? One of my most common use cases is when I want to allow flexible TailwindCSS classes for a specific component feature, but want to better communicate their purpose and constrain with classes are going to be acceptable.
Here is an example where I wanted to allow different gradient colors for a <Tag/>
component to allow posts with multiple tags to have each tag be a different color.
export default function Tag({
children,
gradient = "from-accent to-accent-bright",
}: PropsWithChildren<{ gradient: `from-${string} to-${string}` }>) {
return (
<div className="relative inline-flex">
<div
className={`absolute inset-0 translate-x-1 translate-y-1 rounded-full bg-gradient-to-br ${gradient}`}
/>
<span className="relative rounded-full border px-3 py-1 text-white">
{children}
</span>
</div>
);
}
function InPractice() {
return (
<Tag>Frontend</Tag> // Default
<Tag gradient="from-emerald-500 to-emerald-300">SaaS</Tag> // Custom
<Tag gradient="bg-purple-500">Marketplace</Tag> // Error
)
}
You can even constrain more! For instance we have a FlashMessage component that only allows us to pass in specific values of each color like:
type Pattern = `bg-${string}-500 text-${string}-100`
Hopefully this helps you make safe and flexible props for your React components!