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
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; }'.
styled.d.ts
import "styled-components";
declare module "styled-components" {
export interface DefaultTheme {
fonts: {
size: {
14: string;
12: string;
10: string;
};
}
}
}
theme
objecttheme.ts
import { DefaultTheme } from "styled-components";
const theme: DefaultTheme = {
fonts: {
size: {
14: "0.875rem",
12: "0.75rem",
10: "0.625rem"
},
}
};
export { theme };
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
}
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>
);
}
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])};
`;
My brain hurts ... ;)
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.