Understanding and improving Emotion 10’s TypeScript types

Jake Ginnivan
Pixel and Ink
Published in
7 min readOct 2, 2019

My team recently upgraded from Emotion 9 to Emotion 10. After the upgrade was complete we had a small problem, our compilation time had gone from ~30seconds to ~70 seconds :(

This post is for anyone who wants to get a better understanding of the more advanced TypeScript by following along this deep dive into solving the performance problems facing us by restructuring and rewriting the types to maintain type safety while improving performance.

So where do you start? The first step is to get a baseline and some more information about what is slowing us down.

TypeScript diagnostics flag

The way we do this is to run TypeScript with diagnostics. This emits information about number of files, lines, symbols, types, memory usage and other useful information. Just run:

yarn tsd --diagnostics

Here were the results.

Emotion 9

Files: 2,237
Lines: 308,745
Nodes: 1,049,322
Identifiers: 331,530
Symbols: 1,031,128
Types: 232,302
Memory used: 750,896K
Parse time: 5.07s
Bind time: 1.42s
Check time: 22.07s
Total time: 28.55s

Emotion 10

Files: 2,246
Lines: 313,024
Nodes: 1,056,772
Identifiers: 333,322
Symbols: 1,393,179
Types: 852,685
Memory used: 1,185,914K
Parse time: 5.67s
Bind time: 1.34s
Check time: 52.39s
Total time: 59.40s

Wow, just from upgrading we have gone from ~230k types to ~850k types, and the memory usage jumped from ~750mb to ~1.2gb.

I wanted to rule out us doing something silly in our codebase so I created https://github.com/JakeGinnivan/emotion-types-sandbox. This project has a copy of the emotion types at different versions on different branches with diagnostics in the readme. It would allow me to play around and see what happens.

This gave me a baseline, a simple create react app with a single emotion component.

Emotion 9: 2,793 types, ~1.4 seconds
Emotion 10: 11,015 types, ~2.1 seconds

Great, now I know something has happened in emotion’s types. Lets dig in.

Understanding Emotion 10’s types

Emotion has 2 main ways of interacting with it’s API. An interpolated string with styles in it, and a plain JS object.

So the first thing which is typed is the styled function

This is an interface with 1 generic argument (Theme). It has 2 overloads, which basically are a way to have a single JavaScript function have multiple ways you can call it.

In our case the 2 overloads, one for components (like MyComponent above) and the other is for inbuilt html elements (like 'div' above), overloads unlike making the first tag parameter a union type is that we can have different return types based on the matched input type.

Let’s go with the first overload which TypeScript will select if the first tag parameter matches the type React.ComponentType, a union type which matches both Functional components (React.FC)and Class components (React.ComponentClass). It returns CreateStyledComponentExtrinsic<Tag, ExtraProps, Theme>, which looks like this:

The above is a type alias which maps to CreateStyledComponentBase<PropsOf<Tag>, ExtraProps, Theme. What are CreateStyledComponentBase and PropsOf?

One thing at a time, let’s break down CreateStyledComponentBase, it is a mapped type which is a special type of type alias, which based on the generic argument can return a different type.

What this is saying is “if StyledInstanceTheme not an object, return CreateStyledComponentBaseThemeless<InnerProps, ExtraProps> otherwise return the type CreateStyledComponentBaseThemed<InnerProps, ExtraProps, StyledInstanceTheme>. This works because object extends any == true, object extends {} == true, but object extends { prop: string } == false.

What do the two types which are returned from here look like?

I am going to stop here, to be honest I didn’t actually try and fully understand how this all worked, I was pretty sure I knew my first step.

Rewind to CreateStyled<Theme = any>, if you are using themes you will create your own styled variable with a theme set, like this:

Create styled then returns a type alias which checks to see if I set a theme or not, then returns a type with/without theme support.

Simplification #1

Separate CreateStyled into 2 interfaces, CreateStyled and CreateStyledThemed

Types: 10,976 (initial 11,015 types)
Memory used: 130,982K (initial 121,357K)

Not a great result, sure we dropped the number of types by ~200 but we increased memory usage.

Simplification 2

The main difference between Themed and Themeless is that Themed has a type for theme, and themeless is any. We have this ExtraProps generic argument already, it allows us to make extra props available to the styles, but not require them to be set on the component. Basically library provided props.

Types: 10,426 (initial 11,015 types)
Memory used: 133,237K (initial 121,357K)

Ok, less types, more memory. Well, now that we have a single CreateStyledComponent lets simplify that.

Simplification 3

I have a pretty good idea of what those types are trying to do, but at the end of the day reading those type definitions is way to hard. If I was to boil down what we are trying to do is:

  1. Make props available to our style, some of these are provided by the consumer of the emotion component, others provided by emotion itself.
  2. Return a React component which forces the consumer to specify the props we expect them to pass.

Well, we can easily model that with Props and ExtraProps, the former are props of the component as well as available to styles, the latter is props only available to styles.

Types: 5,180 (initial 11,015 types)
Memory used: 111,624K (initial 121,357K)

Now that is looking better. There is a lesson here I think, keep your types simple, they are easier for you to understand and often the compiler too.

Let’s sum up our interface here. We will look at interpolations next, but leaving them out for the moment.

CreateStyled infers the props for the passed component, ie styled(MyComponent) returns typeof CreateStyledComponent<MyComponentProps, {}>

CreateStyledComponent<MyComponentProps, {}> has 2 overloads, this is to support the two ways of writing styles

You may have noticed that each of the overloads has a generic parameter, this allows you to add extra props to the component (both internal and external), like so

styled(MyComponent)<{ height: number }>(props => ({ height: props.height })

Props will be an intersection type of all 3 generic types, Props, ExtraProps and StyleProps, allowing us to use everything. But we will return a StyledComponent which exposes Props and StyledProps only (Extra props are internal only).

You may ask at this point, why does StyledComponent have 2 generic arguments?

Pretty simple, it allows you to replace the component you are wrapping with another. In this scenario we want to replace the components props, but leave the styled props.

It turned out in the end this was not quite correct because we need to retain the original component props otherwise it’s styles may fail due to missing props. This issue was resolved in another iteration of the types.

Interpolations

The next major part of the libraries types we haven’t gone into are interpolations.

The naming comes from string interpolation and the things which can be included as part of the tagged templates.

In the above example each time we use ${} in our string interpolation the template gets split up and whatever we passed goes into the placeholder.

The tagged template then is responsible for combining the template and the placeholders into a single string. For emotion this means resolving the placeholder into a CSS value. We can restrict the types of the values which can be used in the interpolation by restricting the types of the placeholders variable.

There wasn’t much I could do in this area apart from a few minor changes, so they were mostly left as is.

The final product

The initial pass of the types introduced a few issues, if you dive into the conversations in https://github.com/emotion-js/emotion/pull/1501 you will see a number of them highlighted.

Another issue was the tests had not been running for a while, so fixing those tests ended up being part of this PR.

The tests use https://github.com/microsoft/dtslint which allow you to write comments above lines of code like // $ExpectErroror // $ExpectType SomeType, but in the future it would be good to rewrite them using something like https://github.com/SamVerschueren/tsd which doesn’t rely on tslint (which is being replaced with eslint-typescript).

The main issue with the tests in emotion is they have no context, when something no longer fails it is really hard to know why it was supposed to fail. I have added a bunch of comments into the tests in emotion so hopefully it’s easier for the next person.

I won’t cover the final Type definitions in this blog post as they are just slight modifications and refinements of the initial set of simplifications covered earlier. If you would like me to dive into anything I’ve missed in this post please DM or message me on twitter (https://twitter.com/jakeginnivan).

In the end our project went from ~232k types with Emotion 9’s types, to ~850k types with Emotion 10’s types, back to ~230k types with the types in PR #1501.

I hope this post is useful or the updates to the types decreases your build time. I covered a HEAP of different TypeScript concepts and how they fit together so next time you have to dive into some complex type definitions you have a few more tools in your toolbox to understand them.

--

--

Jake Ginnivan
Pixel and Ink

Co-Founder featureboard.app | Principal Consultant arkahna.io | Previously Tech Lead Seven West Media WA | International Speaker | OSS | Mentor