Search code examples
typescriptreact-nativeparameter-passing

How can I pass a dynamic string as a key to a style in TS react-native?


I want to pass the number of rows (a react native style) dynamically. I tried using 'keyof' as some here have suggested in a different issue, but it didn't work. The code itself works, I'd just like TS to stop screaming at me:

Type '{ backgroundColor: string; flex: number; } | { paddingTop: number | undefined; } | { marginTop: number; fontWeight: "800"; fontSize: number; textAlign: "center"; marginBottom: number; } | { ...; } | ... 7 more ... | { ...; }' is not assignable to type 'StyleProp<ViewStyle>'.

Here's the component:

const Col: React.FC<
  PropsWithChildren<{
    numRows: number;
  }>
> = ({ children, numRows }) => {
  return (
    <View style={styles[`${numRows}col` as keyof typeof styles]}>{children}</View>
  )
}

JSX example:

  <Row>
    <Col numRows={1}>
      <Text>Prompt</Text>
    </Col>
    <Col numRows={3}>
      <Text>This is the prompt</Text>
    </Col>
  </Row>

Here's the relevant style excerpt:

const styles = StyleSheet.create({
//... other styles

  "1col": {
    backgroundColor: "lightblue",
    borderColor: "#fff",
    borderWidth: 1,
    flex: 1
  },
  "2col": {
    backgroundColor: "green",
    borderColor: "#fff",
    borderWidth: 1,
    flex: 2
  },
  "3col": {
    backgroundColor: "orange",
    borderColor: "#fff",
    borderWidth: 1,
    flex: 3
  },
  "4col": {
    backgroundColor: "orange",
    borderColor: "#fff",
    borderWidth: 1,
    flex: 4
  }

});


Solution

  • The typing is incorrect because you can't accept any number of columns; you should only allow the number of columns you actually support. For example, right now you allow passing 3.14159 to numRows, but don't have a style for that number.

    Typescript may be smart enough to check your style names if you limit numRows to only what you do support:

    const Col: React.FC<
      PropsWithChildren<{
        numRows: 1 | 2 | 3 | 4;
      }>
    > = ({ children, numRows }) => {
      return (
        <View style={styles[`${numRows}col`]}>{children}</View>
      )
    }
    

    If not, you can select only the styles you have defined:

    const Col: React.FC<
      PropsWithChildren<{
        numRows: 1 | 2 | 3 | 4;
      }>
    > = ({ children, numRows }) => {
      const columnStyle = (() => {
        switch (numRows) {
          case 1: return styles['1col'];
          case 2: return styles['2col'];
          // etc
        })();
    
      return (
        <View style={columnStyle}>{children}</View>
      )
    }
    

    However, you may not even need these separate styles, due to a misunderstanding of flex. flex tells the style how much space to take up relative to the other elements in its container. All you need in your case is flex: 1.

    If you put any number of elements in your row, all with flex: 1, they will be weighted equally. If you put one element into a row with flex: 4, it will take up the whole row, because the row total is 4 and the element has 4.

    You only need different numbers when you want columns of different widths, for example: 1 item with flex: 2 and two items with flex: 1 in a row will give the first item half the space and the other two a quarter each.