Annotating React Styled Components with TypeScript

Styled components redefine how we apply CSS styles to React components. Unlike the traditional approach of manually assigning CSS classes (from an imported CSS file) to elements within a component, CSS-in-JS libraries like styled-components provide primitives for locally scoping CSS styles to a component with unique, auto-generated CSS classes.

Consider a simple <Card /> component composed of several styled React components. Each of styled's helper methods corresponds to a specific DOM node:

Here, the <Card /> component's styles, structure and logic can all be found within a single file. styled-components comes up with a unique class name for each set of CSS rules, feeds the CSS to a CSS preprocessor (Stylis), places the compiled CSS within a <style /> tag, injects the <style /> tag into the page and adds the CSS classes to the elements.

By tightly coupling components to their styles, we can easily maintain CSS in large React applications. If we need to edit anything about a component, whether it be its color or how it responds to user input, then we can visit one file, which keeps everything related to the component colocated, to make the necessary changes. Unique class names prevent naming collisions with existing class names.

Popular CSS-in-JS libraries, such as styled-components and Emotion, come with TypeScript definitions. When pairing styled components with TypeScript, our React application gains all the benefits of a statically typed language while also ensuring component styles remain well-organized. As we write our styled components, our IDE can warn us of any incorrect arguments passed to any of the helper methods, detect typos, perform autocompletion, highlight missing props, etc.

Below, I'm going to show you how to annotate React styled components with TypeScript.

Installation and Setup#

To get started, scaffold a new React application with the Create React App and TypeScript boilerplate template.

Within this new project, install the styled-components library and @types/styled-components, which provides type definitions for styled-components.

Within the src directory, create a new directory, components, which contains the React application's components.

Within this new directory, create a new file Card.tsx, which contains a <Card /> component. Copy and paste the <Card /> component's source code (from above) into this file.

React Native#

For React Native projects, you would need to install an additional set of type definitions:

Then, you must add styled-components-react-native to the list of types in tsconfig.json:

(tsconfig.json)

Annotating a React Styled Component#

Suppose we wanted to annotate the example <Card /> component previously mentioned.

Annotating a React functional component requires:

  1. Importing the generic FC type (short for FunctionComponent) from react. Since each and every component uses a different set of props, this generic not only lets us specify the unique shape of the props being passed to the component, but also:

    • Has an explicit return type.

    • Autocompletes and type-checks static properties like displayName and propTypes.

    • Gives an implicit definition for the optional children prop.

  1. Defining an interface that describes the shape of the props. Within the interface, we tell TypeScript the type of each prop accepted by the component.

(src/components/Card.tsx)

If we want to add to or override the <Card /> component's styles within a parent component, then we need the <Card /> component to accept an optional className prop. By default, all properties are required, but those marked with a question mark are considered optional.

(src/components/Card.tsx)

Now the <Card /> component can be modified from a parent component.

(src/App.tsx)

Annotating Forwarded Refs#

Suppose a styled component's inner DOM element must be accessed by a parent component. To forward a ref to a styled component, we must pass the styled component to the forwardRef function, which forwards the ref to an inner DOM element that the styled component renders.

Annotating a styled component that receives a forwarded ref involves two steps:

  1. Importing the generic ComponentPropsWithoutRef type.

  2. Extending the props interface with the generic ComponentPropsWithoutRef type. This augments the interface with prop types associated with a certain DOM element, such as type?: 'submit' | 'reset' | 'button' | undefined; and disabled?: boolean | undefined; for <button /> elements. In this case, we augment the interface with prop types associated with a <div /> element (the top-most child element rendered by the <Card /> component and where ref will be attached to). Visit this link to see which <div /> element attributes are annotated and will automatically be added to the CardProps interface.

(src/components/Card.tsx)

The parent <App /> can obtain a ref to the underlying <div /> element and access it whenever it needs to register event listeners on the component, read/edit DOM properties, etc.

(src/App.tsx)

Annotating a Custom Theme and Adapted Styles Based on Props#

styled-components lets you easily customize your React application's theme via the <ThemeProvider /> wrapper component, which uses the context API to allow all children components underneath it to have access to the current theme.

(index.tsx)

When adapting styles based on props, TypeScript automatically recognizes the theme property on the props object.

Below is a screenshot of passing a function to a styled component's template literal to adapt its styles based on its props. Notice that TypeScript raises no warnings or errors when you reference the props object's theme property. In fact, if you happen to use VSCode and hover over props, then a tooltip with its type definition appears.

If you hover over props.theme, then you will see its type definition ThemeProps<any>.theme: any.

The any indicates that TypeScript will not raise any warnings no matter what property you try to access from props.theme, even if it does not exist!

If I reference a property on the props.theme object that might reasonably be available on it like primary, or something ridiculous like helloWorld, then TypeScript will not raise any warnings for either. Hover over any one of these properties, and a tooltip with the type any appears.

By default, all properties on the props.theme object are annotated with the type any.

To enforce types on the props.theme object, you must augment styled-components's DefaultTheme interface, which is used as the interface of props.theme. For now, start by defining the primary property's type as a string (i.e., we might set the primary color of the default theme to red) in DefaultTheme.

(styled.d.ts)

Note: By default, DefaultTheme is empty. That's why all the properties on the props.theme object are annotated with the type any.

Within tsconfig.json, add the newly created styled.d.ts declaration file to the list of included files/directories required by TypeScript to compile the project:

(tsconfig.json)

Now, if you revisit the Card styled component, then you will see TypeScript raising a warning about props.theme.helloWorld since we did not define its type within the DefaultTheme interface.

If you hover over props.theme.primary, then a tooltip with its type definition, DefaultTheme.primary: string, appears.

Plus, if you revisit the index.tsx file, then you will also find TypeScript warning about the theme object being passed to the <ThemeProvider /> wrapper component:

  1. The DefaultTheme interface does not have a type definition for the main property.

  2. The theme object is missing a primary field.

This issue can be resolved by replacing the main property with the primary field:

Better yet, you can import DefaultTheme from styled-components and annotate the object with this interface.

Back inside Card.tsx, you can refactor the Card styled component by setting background-color directly to the current theme's primary color.

To see the final result, visit this CodeSandbox demo:

https://codesandbox.io/embed/keen-satoshi-fxcir?fontsize=14&hidenavigation=1&theme=dark

Next Steps#

Try annotating styled components in your own React applications with TypeScript.

Sources#