Typing Builder Functions in TypeScript

Illustration by me

Styled Components is a library that allows you to define CSS within JS.

It’ll generate all the CSS classes dynamically, then compile it and add it to the HTML on the fly.

In Styled Components you would define a <div /> with a blue background and use it as a React component like so:

You can also easily add new HTML props like so:

This flexibility demonstrates the value of CSS in JS.

If you’ve ever played around with Styled Components, you may have noticed that it has one of the most insane types in the TypeScript ecosystem. It uses the builder pattern of software development and doesn’t just build components, but types as well:

Notice how it’s able to compose together new prop types when using the attrs feature to pre-apply HTML props? This type of composition demonstrates the power of TypeScript.

We’re going to take a page from Styled Components to build our own builder function with memory and maybe even take it a bit further to improve DX (developer experience) along the way.

At the end of this article, we’ll have built the type for a color builder utility that helps us use a color theme palette in our system:

Notice how the modified color is encoded into the type itself! We’ll also be able to add guardrails along the way:

Theming Set Up

First let’s just define a basic structure for theming. Here’s a small set of colors:

Next we’ll configure Styled Components to set the theme object with a function that can build colors for us (ignore the options parameter for now):

Now we can do this:

An implementation for getColor wasn’t shown but assume that it will use the color name to locate the right color and output it in rgba format.

(The consistency of the outputted format is somewhat important since IE11 doesn’t support #rrggbbaa format.)

We can actually build a helper to produce
({theme}) => theme.getColor(theme.colors.brand) for us:

So now we can have something more semantic like:

Much cleaner.

Note
This is not how colorFns should be built. It should actually use getters to generate new functions on each access, but I’ll leave the implementation as a JavaScript exercise. I’ll write a follow-up article on it if I get enough requests.

Options Parameter

Remember the options parameter we ignored? getColor can actually do a bit more than just generate a color. It can tweak the color.

For this exercise, we will support 3 options:

  1. asSolidHex, which allows us to force the color output in hex format, useful for niche IE use cases
  2. alpha, which allows us to set the transparency
  3. asTheme, which allows us to force a specific theme when building a color

So a color like:
colorFns.brand.asSolidHex()
should generate a brand hex color.

colorFns.brand.alpha(0.5)
should generate a 50% transparent brand color.

colorFns.brand.asTheme(‘light’).alpha(0.5)
should generate a 50% light themed brand color.

For this type of DX, it wouldn’t make sense to endlessly chain changes like so:
colorFns.brand.alpha(0.5).alpha(0.1).asSolidHex()

In fact, alpha and asSolidHex are mutually exclusive, so if one exists, the other cannot.

So, how can define the type for something like this?

Deconstruction:

colorFns.brand is essentially a functor, a combination of a function and an object with extra properties, like asSolidHex. You may have noticed that, yes, this is a lot like styled.div.

Foundations

We've already defined a super basic ColorFn. This definition will serve as our base that we’ll build upon:

First, let’s define the types for the constituent components like alpha. Since we’ll be able to chain function calls but still be able to use the result in our CSS like this:

That means that after every function call we’ll still have a StyledFnBase. Meaning that we can build our constituents like so:

With these we can compose the functor type like so:

Since we are not going to define colorFns in this article, we’ll just adjust the colorFns object to be a placeholder like so:

Now we can test, and it’s good so far, look:

But… it’s allowing us to chain endlessly:

How can we block this behavior?

Memories

So far every time we call one of our composition functions we’re re-outputting a ColorFn again, but not all ColorFn are the same.

colorFn.brand.alpha()
is not the same as
colorFn.brand.asTheme(‘light’)

Adding memory will account for this. To do this, we’ll need to make ColorFn a generic so we can modify it. Specifically, it’ll track what’s been applied.

One way to do this is just to use a string of tags like so:

Now to actually use these keys, we can use conditionals:

The reason why we have
Tags extends BaseTags = "alpha" | "asSolidHex" | "asTheme" instead of just = BaseTags is so that we can see the tags like this:

(And this will be important later)

Now take a look at this~

It works! Notice how alpha is no longer available?

But! There’s an issue:

After using asSolidHex it became amnesic. This is because the compositional functions are always returning a ColorFn. We need to make all the compositional functions return a different ColorFn instead.

We can do this by also making them generic, so we can tell it what to return:

We then update our ColorFn compositions to control what happens when each thing is called. To make alpha and asSolidHex mutually exclusive, we’ll remove both options when we use either:

Take a look:

It’s not resetting after each call and it blocked me from calling both alpha and asSolidHex! And if we hover over the variable type we’ll be able to see the remaining tags!

Improving DX (Developer Experience)

Well… if we can see the tags… then why not make them more semantic?

We could let the developer see the attributes of the color being applied:

Great! But if we can remove tags, why not take it one step further by adding new tags? Using a very useful feature in typescript 4.1, template literals, we’ll just tweak our types a bit…

and…

There we go! However, if we exhaust all our options we get:

To fix this we’ll just need to somehow preserve all the context associated with ColorFn, so we’ll just add a dummy property, length, that already exists on StyledFnBase to prevent TypeScript from collapsing the type and simplifying it:

Encoding the Original Color

Ok, let’s take it just one level further.

As a developer, sometimes it’s actually useful to be able to see the actual color being applied, at least for the default theme. This is especially true when you’re working off a design mockup and want to validate your chosen colors against their mockup which would be designed in the default theme.

So let’s just inject the actual hex into the colorFns dictionary type itself:

Done! Playground Link.

Note: If I get enough requests I’ll also write an article on the JavaScript side explaining how to actually define the colorFns dictionary.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store