Search code examples
reactjstypescripthashmapstyled-componentskeyof

Conditionally set attributes in styled-component using props to index on theme object with TypeScript?


Overview

I have a next.js (react) TypeScript project that is using styled-components ThemeProvider. I'm trying index on a theme object that stores rem-based font sizes as strings and are indexed by point sizes (integer). The issue that I'm running into is that I'm not able to dynamically set the index based on a prop passed from the component. Error Message, Code Samples, Attempted Solutions, and Observations follow below

Error Message

Element implicitly has an 'any' type because expression of type 'number' can't be used to index type '{ 14: string; 12: string; 10: string; }'.

  No index signature with a parameter of type 'number' was found on type '{ 14: string; 12: string; 10: string; }'.


Code Samples

Theme Object Type Definition

styled.d.ts

import "styled-components";

declare module "styled-components" {

  export interface DefaultTheme {
    fonts: {
      size: {
        14: string;
        12: string;
        10: string;
      };
    }
  }
}


Styled-Copmponents theme object

theme.ts

import { DefaultTheme } from "styled-components";

const theme: DefaultTheme = {
  fonts: {
    size: {
      14: "0.875rem",
      12: "0.75rem",
      10: "0.625rem"
    },
  }
};

export { theme };


Styled-Component

components.ts

import styled from "styled-components";

const StyledThingie = styled('div')`
  /* 
     HERE IS WHERE THIS ISSUE IS 
     I want to conditionally inject the prop (if fontSize is passed) 
     into the size index to get the rem-based size, and set a default 
     font-size if no prop is passed.

     props.theme.fonts.size[10] works just fine.
  */
  font-size: ${props => (props.fontSize ? props.theme.fonts.size[props.fontSize] : props.theme.fonts.size[10])};
`;

export {
  StyledThingie
}


React Functional Component

Thingie.tsx

import React from "react";
import styled from "styled-components";
import { StyledThingie } from "./components.ts";

const Thingie = styled(StyledThingie)`
  display: block;
`;

const ThingieComponent: React.FC<any> = (props) => {
  return(
    <Thingie fontSize={14}> // HERE IS WHERE I"M PASSING THE PROP
      <p>This paragraph should be set to 0.875rem or theme.fonts.size[14] </p>
    </Thingie>
  );
}


Attempted Solutions

I tried to define an index signature according to some of the TypeScript docs, but wasn't clear how to implement this. Here's what I attempted:

interface FontSize {
    [index: string]: number;
}

export type StyledThingieProps = {
    fontSize?: FontSize;
}

const StyledThingie = styled('div')<StyledThingieProps>`
  size: ${props => (props.fontSize ? props.theme.fonts.size[props.fontSize] : props.theme.fonts.size[10])};
`;


Observations

My brain hurts ... ;)


Solution

  • That error is caused when you use a variable to index a type with non-dynamic properties. So to get around it, you need to change the type declaration of DefaultTheme.fonts.size to have a definition for dynamic properties. (Basically, the type checker has no way to know if the variable passed to the index ([]) is going to exactly match one of the declared keys. So you have to say "I'm ok with any numeric or string key".)

    if you still want to declare the known keys:

      export interface DefaultTheme {
        fonts: {
          size: {
            [key: number]: string;
            14: string;
            12: string;
            10: string;
          };
        }
    

    But, if you don't care about what the specific font sizes are, you can just do

      export interface DefaultTheme {
        fonts: {
          size: {
            [key: number]: string;
          };
        }
    

    Those two options fix the type definition to accept any number as the key. Alternatively, you can coerce the fontSize into a key type (using keyof), which is basically an enum which has all the keys of the object as the values. You are telling the type checker "this variable should be any of these specific values (any of the keys in the specific type)":

    // in the template where you use the font size
    props.theme.fonts.size[props.fontSize as keyof typeof props.theme.fonts.size]
    // or using the type declaration directly
    props.theme.fonts.size[props.fontSize as keyof DefaultTheme["font"]["size"]]
    

    You were also on the right track by declaring the type of font size, but you got the syntax a bit wrong:

    export type StyledThingieProps = {
        fontSize?: keyof DefaultTheme["font"]["size"];
    }
    

    That's another way to fix it.