Typing Builder Functions in TypeScript
Featuring Styled Components

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:
asSolidHex
, which allows us to force the color output in hex format, useful for niche IE use casesalpha
, which allows us to set the transparencyasTheme
, 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 ascolorFn.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.