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 themeconst { 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 whichas
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.