Will King
How-to

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!

Updates and More

Get updated when new articles, products, or components are released. Also, whatever else I feel like would be fun to send out.