2

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 ... ;)

rv-bnowak
  • 23
  • 3

2 Answers2

2

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.

Garrett Motzner
  • 3,021
  • 1
  • 13
  • 30
  • @GarretMotzner you are awesome. Wish I had more rep to kick the upvotes, but alas I'm scrubby as far as SO is concerned. Thank you so much, all of that worked like a charm! – rv-bnowak Mar 04 '20 at 14:57
  • @GarrettMotzner can I have the best of both worlds? Is it possible to say something like "if the fontSize prop exists, make sure it's one of the keys in the type, but if the fontSize prop doesn't exist (undefined) to make it optional? – Zach May 08 '21 at 00:27
  • @Zach I imagine you can, most likely. What have you tried to make that work, and what error did you get? – Garrett Motzner May 14 '21 at 16:35
0

I solved it in the following way:

export const TextInput = styled.TextInput.attrs(({theme}) => ({ placeholderTextColor: theme.colors.primary, }))
font-family: ${({ theme }) => theme.fonts.interBold};
font-size: 18px;
color: ${({ theme }) => theme.colors.primary};
;