My Experience Creating Strongly Typed styled-components With React + TypeScript

Vakhtang Nodadze
4 min readDec 21, 2020

There’s been a big hype about TypeScript and how it fixes many issues that JS has. So I just recently decided to try it and see for myself. Like many of you in my situation, I struggled for some time, but nevertheless, I found its strict rules very useful and handy. You might struggle at first, but you get the benefits afterward.

I am working on a small private project, trying to build a sorting visualizer using different algorithms. I stopped in midway because I wanted to integrate TypeScript into the build. I am a big fan of styled-components, so that’s a double win for me. I could use TypeScript to help me with strongly typed themes and props.

Little did I know there were pain and suffering waiting for me.

//MyComponent.styled.tsx for MyComponent.tsx
const StyledContainer = styled.div<StyledContainerProps>`
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
flex-direction: row;
flex-grow: 1;
width: calc(100% + ${(props) =>
props.theme.spacing[props.spacing]}px);
margin: 0 -${(props) => props.theme.spacing[props.spacing]}px;
${(props) => props.spacing &&
css`
& > .item {
padding: ${({theme}) => theme.spacing[props.spacing]}px;
}
`};
`;

This how one of my styled-components looks. So basically, I am waiting for props of spacing and theme so it can take width, margin, and padding accordingly. If you are familiar with styled-components, creating your own theme would mean using ThemeProvider, wrapping it around your component, and passing the ‘theme’ props to the component itself. So it would look something like this.

//MyComponent.tsximport { withTheme } from 'styled-components';const MyComponent = ({theme}) => {
return (
<div style={{margin: theme.spacing.}}>
<h1>This is my Component</h1>
</div>
);
};
export default withTheme(MyComponent);

That’s cool but how would you wrap a styled component? There’s just no way. One solution you can do is create an interface of your theme, and pass THAT as props.

//Theme.tsx
interface Spacing {
[key: number]: string;
noSpacing: number;
tiny: number;
mini: number;
small: number;
default: number;
big: number;
large: number;
extraLarge: number;
huge: number;
extraHuge: number;
}
interface ThemeProps {
theme: {
spacing: Spacing;
}
};
interface Props extends ThemeProps {
spacing: string;
}

Tried it, it didn’t complain, but it didn’t give me strong typing either, and that’s what we’re here for right?

The one solution I came across after scouring multiple threads was this. Your Theme file should look something like this:

//Theme.tsx
import baseStyled, { ThemedStyledInterface } from 'styled-components';
export interface Spacing {
[key: number]: string;
noSpacing: number;
tiny: number;
mini: number;
small: number;
default: number;
big: number;
large: number;
extraLarge: number;
huge: number;
extraHuge: number;
}
export interface ThemeProps {
theme: {
spacing: Spacing;
}
};
export const spacing = {
noSpacing: 0,
tiny: 2,
mini: 4,
small: 6,
default: 8,
big: 16,
large: 24,
extraLarge: 32,
huge: 38,
extraHuge: 44
};
export const theme = {
spacing: spacing
};
export type Theme = typeof theme;
export const styled = baseStyled as ThemedStyledInterface<Theme>;

For simplicity's sake, I create an object of spacing, because we are going to need it later also and of course, your root component should have a ThemeProvider:

// App.tsx
import { ThemeProvider } from 'styled-components';
import { MyComponent } from './MyComponent';
import { theme } from './Theme';

export const App = () => {
return (
<ThemeProvider theme={theme}>
<MyComponent />
</ThemeProvider>
);
};
//MyComponent.styled.tsx for MyComponent.tsx
import { spacing, styled } from '../../../styled/themes';
interface StyledContainerProps {
spacing: keyof typeof spacing;
}
const StyledContainer = styled.div<StyledContainerProps>`
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
flex-direction: row;
flex-grow: 1;
width: calc(100% + ${(props) =>
props.theme.spacing[props.spacing]}px);
margin: 0 -${(props) => props.theme.spacing[props.spacing]}px;
${(props) => props.spacing &&
css`
& > .item {
padding: ${({theme}) => theme.spacing[props.spacing]}px;
}
`};
`;

Basically, what we are doing is creating our own ‘styled’ object which we can use to bind to the theme properties. In my case, I have default props for spacing, so if the coder doesn’t pass it, it will take that default value. This type of component is very useful if you are planning on creating your own custom components.

One thing to keep in mind when dealing with TypeScript and passing props as object key values, always use the types you are using in the theme interface. The easiest way I could find is to just import the object from the themes and tell your props that this the key of the type spacing as you see above.

I decided to write this article because I couldn’t find any good ones explaining how to best integrate TypeScript with styled-components. If you are going to use both in your project, why not receive the benefits of both.

Thank you for reading. Hope you find this helpful.

For the code you can visit: https://github.com/VakhoNodadze/Sorting_Visualizer

--

--

Vakhtang Nodadze

Software engineer with 3+ years of experience. Passionate about my work and the industry in general.