I’m trying to make a function (react hook) which wraps StyleSheet.create(), in argument it should take style like object, but with my custom theme values and it should be fully typed with typescript, for example:
function useStyle(style){
const theme = useTheme();
const newStyle = ...
// then translate object to StyleSheet.create() type
return StyleSheet.create(newStyle)
}
const style = {
container: {
margin: "small",
backgroudColor: "background"
},
text: {
color: "text",
fontSize: "medium"
}
}
const styleObject = useStyle(style)
// styleObject type should be
container: {
margin: number,
backgroudColor: string
},
text: {
color: string,
fontSize: number
}
}
So far I’m stuck with my simplified example:
type RNViewStyle = {
margin?: number;
}
type RNTextStyle = {
fontSize?: number;
}
type ViewStyle = {
margin?: keyof Theme['spacing'];
}
type TextStyle = {
fontSize?: keyof Theme['font']['size'];
}
type Style<T> = {
[Key in keyof T]: ViewStyle | TextStyle;
};
type TranslatedStyle<T> = {
[Key in keyof T]: RNViewStyle | RNTextStyle
}
type Theme = {
spacing: {
small: number;
medium: number;
large: number;
}
font: {
size: {
small: number;
medium: number;
large: number;
}
}
}
const theme: Theme = {
spacing: {
small: 10,
medium: 20,
large: 30,
},
font: {
size: {
small: 10,
medium: 20,
large: 30,
}
}
}
const translateStyle = <T extends Style<T>>(styles: T): TranslatedStyle<T> => {
const newStyles: TranslatedStyle<T> = {} as TranslatedStyle<T>;
for (const key in styles) {
const style = styles[key];
const newStyle: RNViewStyle | RNTextStyle = {};
if (style.margin) {
newStyle.margin = theme.spacing[style.margin]
}
if (style.fontSize) {
newStyle.fontSize = theme.font.size[style.fontSize]
}
newStyles[key] = newStyle;
}
return newStyles;
}
const style = translateStyle({
container: {margin: 'small'},
text: {fontSize: 'small'}
});
I’m getting errors on style.margin
and style.fontSize
branches
And this is my React Native example, where ViewStyle | TextStyle | ImageStyle
are my custom valued styles
export type FontWeight =
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900'
| 'bold'
| 'normal';
export type FontTheme = {
family: string;
size: {
small: number;
medium: number;
large: number;
jumbo: number;
};
weight: {
light: FontWeight;
regular: FontWeight;
medium: FontWeight;
bold: FontWeight;
};
};
export type SpacingTheme = {
none: number;
small: number;
medium: number;
large: number;
};
export type ColorTheme = {
background: string;
foreground: string;
border: string;
text: string;
textInverse: string;
primary: string;
secondary: string;
attention: string; // call to action
toned: string; // grey
success: string; // green
warning: string; // yellow
error: string; // red
info: string; // blue
facebook: string;
google: string;
apple: string;
};
export type Theme = {
isDark: boolean;
font: FontTheme;
spacing: SpacingTheme;
colors: ColorTheme;
};
export type KeyofFontSizeTheme = keyof FontTheme['size'];
export type KeyofFontWeightTheme = keyof FontTheme['weight'];
export type KeyofSpacingTheme = keyof SpacingTheme;
export type KeyofColorTheme = keyof ColorTheme;
// Style Types
export type ViewStyle = SpacingStyle &
AlignmentStyle &
FlexStyle &
BorderStyle &
DimensionStyle &
PositionStyle &
BackgroundStyle;
export type ImageStyle = {};
export type TextStyle = AlignmentStyle &
SpacingStyle & {
fontSize?: KeyofFontSizeTheme;
fontWeight?: KeyofFontWeightTheme;
fontColor?: KeyofColorTheme;
textAlign?: 'left' | 'right' | 'center' | 'justify';
};
export type MarginStyle = {
margin?: KeyofSpacingTheme;
marginVertical?: KeyofSpacingTheme;
marginHorizontal?: KeyofSpacingTheme;
marginTop?: KeyofSpacingTheme;
marginRight?: KeyofSpacingTheme;
marginBottom?: KeyofSpacingTheme;
marginLeft?: KeyofSpacingTheme;
};
export type PaddingStyle = {
padding?: KeyofSpacingTheme;
paddingVertical?: KeyofSpacingTheme;
paddingHorizontal?: KeyofSpacingTheme;
paddingTop?: KeyofSpacingTheme;
paddingRight?: KeyofSpacingTheme;
paddingBottom?: KeyofSpacingTheme;
paddingLeft?: KeyofSpacingTheme;
};
export type SpacingStyle = MarginStyle & PaddingStyle;
export type AlignmentStyle = {
align?: 'center' | 'start' | 'end';
};
export type FlexStyle = {
flex?: boolean | number;
direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
wrap?: 'wrap' | 'nowrap' | 'wrap-reverse';
justify?:
| 'center'
| 'start'
| 'end'
| 'space-between'
| 'space-around'
| 'space-evenly';
overflow?: 'visible' | 'hidden' | 'scroll';
gap?: KeyofSpacingTheme;
rowGap?: KeyofSpacingTheme;
columnGap?: KeyofSpacingTheme;
};
export type BorderStyle = {
borderColor?: KeyofColorTheme;
borderWidth?: number;
borderTopWidth?: number;
borderRightWidth?: number;
borderBottomWidth?: number;
borderLeftWidth?: number;
borderRadius?: number;
};
export type DimensionStyle = {
width?: number | `${number}%`;
minWidth?: number | `${number}%`;
maxWidth?: number | `${number}%`;
height?: number | `${number}%`;
minHeight?: number | `${number}%`;
maxHeight?: number | `${number}%`;
};
export type PositionStyle = {
position?: 'absolute' | 'relative';
top?: number;
right?: number;
bottom?: number;
left?: number;
};
export type BackgroundStyle = {
backgroundColor?: KeyofColorTheme;
};
import NamedStyles = StyleSheet.NamedStyles;
export type Styles<T> = {
[P in keyof T]: ViewStyle | TextStyle | ImageStyle;
};
export function useStyles<T extends Styles<T>>(styles: T): NamedStyles<T> {
const theme = useTheme();
return useMemo(() => {
const returnStyles: NamedStyles<T> = {} as NamedStyles<T>;
// Translate styles to valid react native style objects
return StyleSheet.create(returnStyles);
}, [styles, theme]);
}
2
Answers
I'm trying to refactor my hook:
To:
In my project I was trying to enforce types too. Then after realising that I created a huge boilerplate only for myself, I decided to relax requirements and go for an easier solution. I know that this is not a perfect answer, but at least I can share you some experience.
I created this:
Instead of relying on pre-made components like
<Text />
or<Button />
I made my own "branded" components like<ButtonPrimary />
,<Title />
,<Subtitle />
,<Text />
, ` and then used inside them all the defined styles in AppDimensions.So for example, my
<SubTitle />
component would be:I’m not enforcing the style: I’m just indicating the "right path" to use my custom components that are already styled. This makes the code cleaner to read (
Subtitle
is better thanText with size = 20
) and you don’t have to thing about the right typing: just the right use of it and the correct components to have.Need a special button? Create the
<ButtonSpecial />
and reuse it, but remember to use the AppDimension thing!If for any reason you need to use dimensions for example for margins, programmers will know that inside
AppDimensions.Margin.*
they will find the exact value they need, maintaining your view spacing always the same. And if something will go in the "wrong" direction (example hot-fixes, a component that needs to be very custom or other things…) you can still apply for your classicalstyle
value with any number, instead of trying to ignore your enforced typings.This is also a way to get all the cases when your values were "non-standard", aka magic numbers or margins/paddings with some magical stuff in there (Views with padding 12 then 17 then 22).
If you still want to go with your path, I suggest you to simplify the objects into small types and define immediately the values that your style needs to have.
Only then try to compose them into bigger types, so you have the right granularity to see what’s going wrong.