a few thoughts about

Typescript UI Component

Screenshot of Typescript UI Component landing page
Roy Anger
21 November, 2022
4 min read
638 words

Step 1

The first thing we want to do is establish some basic styles and options for our component. In this case we're building a <Title> component, which will display H1 through H6 tags depending on the props passed. I am also going to add in a few basic variants.

We're going to create two objects. The first will use h1 through h6 as the keys. The key is that these are valid HTML tabs and we will see why a little further down. As a note I'm using Tailwind CSS, but you could use regular CSS classes here too.

const types = {
   h1: 'text-4xl md:text-5xl font-title mt-6 mb-2',
   h2: 'text-3xl md:text-4xl font-title mt-4 mb-2 text-slate-600',
   h3: 'text-2xl font-title mt-4 mb-2 p-2',
   h4: 'text-2xl font-sans mt-3',
   h5: 'text-xl font-sans mt-3',
   h6: 'text-lg font-sans mt-3',
}

const variants = {
   default: 'font-normal',
   bold: 'font-bold',
}

Step 2

With that done, let's setup a TypeScript interface and use those two objects. We're going to take in children which will be the text (or whatever else) we want to display as the title. Then we will take in the type, which is one of the h1 through h6 tags. Here we use keyof typeof types instead of something like string. This will convert the keys from the types object into the only valid input for the type prop. In other words if someone tried to do type='img' they would get a Typescript error.

Why not use h1 | h2 | h3 | h4 | h5 | h6? We definitely could. These 6 options have existed since HTML+ in 1993 and there are no plans to add to them. However if we were using a different list of tags for this then after writing the initial component a new tag might get added at some point. Even with this component a <p> might get added when the design calls for something to look kind of like a title but not be one. But using keyof typeof types then as soon as a new option is added to the types object it is also immediately a valid option to use as a prop.

Of course, all of the above also applies to the variant prop, so we can do the same thing with it. The key difference here is that variant is optional. Lastly we have a css prop which is optional and exists in case there is an edge case where we need to pass some one off CSS to the compoenent.

interface TitleProps {
   children: React.ReactNode
   type: keyof typeof types
   variant?: keyof typeof variants
   css?: string
}

Step 3

With all that done, let's build the whole component. We'll make the variant prop optional with a default value of default. In the JSX we can use the clsx package to make the various class inputs neat.

import clsx from 'clsx'

const types = {
   h1: 'text-4xl md:text-5xl font-title mt-6 mb-2',
   h2: 'text-3xl md:text-4xl font-title mt-4 mb-2 text-slate-600',
   h3: 'text-2xl font-title mt-4 mb-2 p-2',
   h4: 'text-2xl font-sans mt-3',
   h5: 'text-xl font-sans mt-3',
   h6: 'text-lg font-sans mt-3',
}

const variants = {
   default: 'font-normal',
   bold: 'font-bold',
}

interface TitleProps {
   children: React.ReactNode
   type: keyof typeof types
   variant?: keyof typeof variants
   css?: string
}

export const Title: React.FC<TitleProps> = ({
   children,
   type,
   variant = 'default',
   css,
}) => {
   const Type = type as keyof JSX.IntrinsicElements

   return (
      <div
         className={clsx(
            types[type],
            variants && variants[variant],
            css ? css : ''
         )}
      >
         <Type>{children}</Type>
      </div>
   )
}

That's it. Obviously you can adjust the JSX as need to suit the needs of your component. Updating this is simple and the TypeScript use of keyof typeof, while simple, helps to reduce errors on update and make the allowed options for the props super clear.

Last edited: 21 November, 2022