skip to Main Content

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


  1. Chosen as BEST ANSWER

    I'm trying to refactor my hook:

    const styles = useStyles(theme => ({
    container: {
      ...getAlign(style),
      ...getBackground(style, theme.colors),
      ...getBorder(style, theme.colors),
      ...getSize(style),
      ...getSpacing(style, theme.spacing),
      ...getView(style, theme.spacing),
    },
    }));
    

    To:

    const styles = useStyles({
    container: {
      align: 'center',
      background: 'background',
      border: 'none',
      padding: 'medium',
      margin: 'large',
    }});
    

  2. 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:

    function createSize(size: number): WidthHeight {
      return { width: size, height: size };
    }
    
    export const AppDimensions {
      Icon: {
        small: createSize(14)
        big: createSize(17)
        ...
      },
      Thickness: {
        thin: 1
      },
      Radius: {
        small: 16,
        big: 64
        ...
      },
      Font: {
        tiny: 14,
        normal: 17,
        ...
      }
    }
    

    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:

    <Text style={{ fontSize: AppDimensions.Font.big }}>{text}</Text>
    

    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 than Text 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 classical style 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.

    const Fonts = {
      small: 12,
      medium: 14,
      large: 16,
      huge: 20,
    };
    type FontSize = keyof typeof Fonts;
    
    // Example of usage
    function fontSizeToNumber(fontSize: FontSize) {
      return Fonts[fontSize];
    }
    

    Only then try to compose them into bigger types, so you have the right granularity to see what’s going wrong.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search