I'm creating a bunch of styled components ins MUI v5 that have some extra props defined on them.
const SomeContainer = styled(Box, {
name: "SomeContainer",
slot: "Root"
})<BoxProps & { whatever: string }>(({ theme, someComponentProp }) => ({
// styles
}));
I would now like to separate the function that returns the styles object into a separate theme file so that I would have SomeContainer.tsx
and SomeContainer.styles.ts
files pair
The content of the styles file would export the styles:
export const SomeComponentStyles: ??? = ({ theme, someComponentProp }) => ({
// styles
});
MUI's Typescript type definitions are very convoluted, and I can't seem to correctly define the type of my export (the ???
part). Sure you can always set configuration to be less strict so you can get away with any
or not even providing the type, but that won't help with vscode intellisense to improve developer's experience writing these styles.
The problem is not, that the code would not compile, but to actually define this type to have a much better dev environment (i.e. vscode) experience when writing these files for various different components? The IDE would provide correct code hints for function parameter objects as well as for the styles themselves within the resulting object.
styled
itself is a function
that returns a function
that takes an arbitrary number of parameters (objects with style definitions or functions that return such objects - my case). My exported function
is one of those parameters.
It seems this should be the correct type, but it's not.
type StyleDefinition = Parameters<ReturnType<typeof styled>>[0];
There's also the additional problem that I'm having with generic function definition where I provide the type as <BoxProps & { whatever: string }>
which means that my extra props should propagate to the function parameters.
Here's a Sandbox to play around.
You were actually almost there, but the culprit is the function overload of styled(Component, options)<AdditionalProps>()
, which makes manipulating its type awkward.
TL; DR:
// Use argument at index 1 (instead of 0)
Parameters<ReturnType<typeof styled>>[1];
When directly executing the styled(Component)()
function, TypeScript is able to infer the correct function overload to be used.
In your case, you are using the 2nd / 4:
<AdditionalProps extends {}>(
...styles: Array<
Interpolation<ComponentProps & SpecificComponentProps & AdditionalProps & { theme: T }>
>
): StyledComponent<ComponentProps & AdditionalProps, SpecificComponentProps, JSXProps>;
But when trying to define the argument separately, trying to extract its type with TypeScript Parameters
built-in utility type, TS uses only the last function overload, as described in Typescript pick only specific method from overload (to be passed to Parameters<T>)
Therefore we get overload 4/4:
<AdditionalProps extends {}>(
template: TemplateStringsArray,
...styles: Array<
Interpolation<ComponentProps & SpecificComponentProps & AdditionalProps & { theme: T }>
>
): StyledComponent<ComponentProps & AdditionalProps, SpecificComponentProps, JSXProps>;
Hence when trying to get the 1st parameter (at index 0, the one used when we directly execute the function), we get TemplateStringsArray
type, instead of the one that is inferred when calling the function.
Fortunately, we see that @mui team provided a reasonable API, where the rest parameter is actually the same as in a previous overload: therefore we can simply get the next one, instead of the 1st (i.e. skip index 0):
// Use argument at index 1 (instead of 0)
Parameters<ReturnType<typeof styled>>[1];
There's also the additional problem that I'm having with generic function definition where I provide the type as
<BoxProps & { whatever: string }>
which means that my extra props should propagate to the function parameters.
We can simply define the function with its actual Component and AdditionalProps ahead, then use its type:
const createStyledBox = styled(Box, {
name: "SomeContainer",
slot: "Root"
})<BoxProps & { whatever: string }>;
type BoxStyles = Parameters<typeof createStyledBox>[1];
const boxStyles: BoxStyles = ({ theme, whatever }) => ({
// styles
padding: theme.spacing(1.5),
// ^? Theme
backgroundColor: whatever === "val" ? "white" : "black",
// ^? string
});
const SomeContainer2 = createStyledBox(boxStyles);
If you want to further decouple the style from the styled component creation, you can indeed define a general type (possibly with generics), and let TypeScript check the structural compatibility when you use it with the creator function:
import { Interpolation, styled, Theme } from "@mui/system";
type InterpolationWithTheme<Props extends {}> = Interpolation<Props & { theme: Theme }>
const whateverStyles: InterpolationWithTheme<{ whatever: string }> = ({ theme, whatever }) => ({
// styles
padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`,
// ^? Theme
backgroundColor: whatever === "val" ? "#fdd" : "white",
// ^? string
});
const SomeContainer3 = styled(Box, {
name: "SomeContainer",
slot: "Root"
})<BoxProps & { whatever: string }>(whateverStyles); // Okay