Opensourcery: Theme types

Types are actually some of the hardest parts of getting a libary correct. Stitches is a css in js (and most importantly ts) library that has an incredible set of types so let's dive into one of the most interesting ones.

Any sufficiently advanced technology is indistinguishable from magic

  • Arthur C. Clarke

How stitches solved their theming types feels like advanced typescript. Luckily, they've shared their magic with us. Let's dive in.

Stitches theme

Among other things, Stitches lets you define your own theme. So for color we could define a set of colors blue-100, blue-200 and then use them in our component as a css prop.


import { createStitches } from '@stitches/react'
export const { styled, css, getCssText } = createStitches({
theme: {
colors: {
'blue-100': '#DBEAFE',
'blue-200': '#BFDBFE'
},
})

Then we can make a button that's fully typed using object styles:


const Button = styled('button', {
color: '$blue-100',
})

The basics

The first thing to do is define what the theme is initially made up of. In our case we just have a colors scale that's used by color. This is the theme that'll be used by our function:


declare namespace Config {
export type Theme<T = {}> = {
colors?: { [key in string]: string }
}
}

Define a function

For our example we want a const { styled } = createStitches({theme}) typed function. This is just pseudo code and we'll start adding to it:


export type CreateStitches = {
(config): {
styled(elementToStyle, styleObject)
}
}

Dynamic theme

What we want is to allow one of the keys of the theme to be used. To do this we need the generics. Theme will be inferred from the passed in value.


export type CreateStitches = {
<Theme extends Config.Theme>(config?: { theme?: Theme }): {
styled: (elementToStyle, styleObject) => void
}
}

Generic return

We want the value passed into the styled function for color to be one of the colors from the theme. To do this we can use the keyof to pull out the keys.


export type CreateStitches = {
<Theme extends Config.Theme>(config?: { theme: Theme }): {
styled: (
elementToStyle: string,
{ color }?: { color?: keyof Theme['colors'] }
) => void
}
}

Aside: elementToStyle

It'd be nice to also restrict elementToStyle down to all the JSX elements such as div, button etc. To do this we can add


( elementToStyle: keyof JSX.IntrinsicElements, /**...*/)

Some javascript

Finally we can use this type and all of the dynamic properties:


const createStiches: CreateStitches = ({ theme }) => {
return {
styled: (type, { color }) => /*...*/,
}
}
// call it with our theme
const { styled } = createStiches({
theme: {
colors: {
'blue-100': 'blue1',
'blue-200': 'blue2',
},
},
})
// types are suggested and can be autocompleted!
styled('button', {
color: 'blue-100',
})

This solves the token key problem, but stitches also maps to ALL of the other color values as well AND includes the css colors like royalblue and rebeccapurple and allows other string values #fff.

Challenge #1 - allow string types

The simplest thing to do to allow string types would be to change our type to also include colors right?


{ color }?: { color?: keyof Theme['colors'] | string }

This works -- kinda. Now we can use blue-100 or #00f but now we don't get any suggestions of our own theme values. Oof indeed.

Challenge #1b - allow string types BUT still allow for type checking

The unsung hero csstype can actually come to our rescue here because if we look at any lowly <div style={{ color: 'rebeccapurple'}}> we can see it allows strings AND does css spec defined values as well.


import type * as CSSType from 'csstype'
type Color = CSSType.Properties['color']
const c: Color = 'cornflowerblue'

Challenge #2 - allow scale values

So to do this we can just refer to the original parent scale or our theme value:


{ color }?: { color: CSSType.Properties['color'] | keyof Theme['colors'] }

So we finally have our type that allows us to use our theme values AND existing css types.

Prefix tokens with $

Lastly, Stitches adds a $ in front of any token values. They have a type utility that does this:


export type Prefixed<K extends string, T> = `${K}${Extract<
T,
boolean | number | string
>}`

and now we can use the utility to have our auto-complete show us the $blue-100 version instead


export type CreateStitches = {
<Theme extends Config.Theme>(config?: { theme?: Theme }): {
styled: (
type: keyof JSX.IntrinsicElements,
{
color,
}?: {
color:
| CSSType.Properties['color']
| Prefixed<'$', keyof Theme['colors']>
}
) => void
}
}
//...
styled('button', {
color: '$blue-100',
})

We're not done yet

This is already a big step forward but there's a few more type features provided by stitches that would be nice to understand. Stay tuned for future posts on:

  • how did they get one scale used for multiple scales?

const Button = styled('button', {
backgroundColor: 'blue-100', // magic
})

  • how are utility functions defined AND how can we infer the type passed into the utility function to know the right scale for it?
  • how are variants defined AND type safe for the defaults?
  • how does the as property allow changing the react props depending on which as is chosen? material-ui also has this

The Stitches code is great and also has so much type sorcery happening under the covers so I'd recommend reading through the /types folder to explore how much of the typing is done.