如何键入一个接受接口并转换为另一个接口的函数?

How to type a function which takes interface and translates to another one?

提问人:Michal Fedyna 提问时间:11/13/2023 更新时间:11/14/2023 访问量:66

问:

我正在尝试制作一个包装 StyleSheet.create() 的函数(react hook),在参数中它应该采用像 object 一样的样式,但使用我的自定义主题值,并且应该完全使用 typescript 键入,例如:

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
  }
}

到目前为止,我坚持我的简化示例:

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'}
});

我在分支上遇到错误style.marginstyle.fontSize

这是我的 React Native 示例,我的自定义值样式在哪里ViewStyle | TextStyle | ImageStyle

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]);
}
reactjs 打字稿 react-native react-typescript

评论

0赞 jcalz 11/13/2023
并且是并且应该是相互排斥的?或者某物可以同时存在吗?如果它们不必相互排斥,我会做这样的事情。这能满足您的需求吗?如果是这样,我可以写一个答案来解释;如果没有,我错过了什么?ViewStyleTextStyle
0赞 Michal Fedyna 11/14/2023
它们必须是相互排斥的,因为 StyleSheet.create() 采用 React Nativetype NamedStyles<T> = {[P in keyof T]: ViewStyle | TextStyle | ImageStyle};
0赞 jcalz 11/14/2023
联合在 TypeScript 中不是排他性的,因此它们不必相互排斥。这就是我问的原因。一个相互排斥的联合看起来更像是.此外,您编写了 instead 而不是,因此您的代码似乎认为它们也可能不是排他性的。您可以编辑以澄清(例如,如果它们是排他性的,请使用)吗?{ margin: number, fontSize?: never} | { margin?: never, fontSize: number}if (style.margin) { } if (style.fontSize) { }if (style.margin) { } else if (style.fontSize) { }else
0赞 Michal Fedyna 11/14/2023
也许我不明白,因为英语不是我的母语,但主要思想是,例如,第一个示例中,type 是 ViewStyle 并且是 TextStyle,因为不同的组件采用不同的道具,我正在尝试创建一个函数,将我的对象与我的自定义主题相关的值转换为绝对值,同时对类型进行相同的翻译containertitlestyle

答:

0赞 Martinocom 11/14/2023 #1

在我的项目中,我也试图强制执行类型。然后,在意识到我只为自己创造了一个巨大的样板之后,我决定放宽要求并寻求更简单的解决方案。我知道这不是一个完美的答案,但至少我可以分享一些经验。

我创建了这个:

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,
    ...
  }
}

我没有依赖预制组件,比如 或者 我制作了自己的“品牌”组件,比如 、 、 、 ' ,然后在其中使用 AppDimensions 中所有定义的样式。<Text /><Button /><ButtonPrimary /><Title /><Subtitle /><Text />

例如,我的组件是:<SubTitle />

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

我不是在强制执行样式:我只是指示使用已经设置样式的自定义组件的“正确路径”。这使得代码读起来更干净(比 更好),而且您不必考虑正确的类型:只需正确使用它和拥有正确的组件即可。SubtitleText with size = 20

需要一个特殊的按钮吗?创建并重用它,但请记住使用 AppDimension 的东西!<ButtonSpecial />

如果出于任何原因您需要使用尺寸,例如边距,程序员会知道,他们会在里面找到他们需要的确切值,从而始终保持视图间距不变。如果某些事情会朝着“错误”的方向发展(例如热修复程序,需要非常自定义的组件或其他东西......),您仍然可以使用任何数字来应用您的经典值,而不是试图忽略您的强制类型。AppDimensions.Margin.*style

这也是一种获取您的值为“非标准”的所有情况的方法,即魔术数字或带有一些神奇内容的边距/填充(填充 12 然后是 17 然后是 22 的视图)。


如果你仍然想走你的路,我建议你把对象简化为小类型,并立即定义你的风格需要的值。

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];
}

只有这样,才能尝试将它们组合成更大的类型,这样您就有正确的粒度来查看出了什么问题。

评论

0赞 Michal Fedyna 11/14/2023
我正在做类似的事情,但我的主题是从可能更改的 react 上下文中检索的动态值,我不知道您的方法是否适用于我
0赞 Michal Fedyna 11/14/2023
例如,您可以在此处查看我的代码
0赞 Martinocom 11/14/2023
啊,好的,我明白了。如果您的 GUI 选择以某种方式被引导,例如您可以选择 4 种字体大小之一,它仍然可以工作。<Select />
0赞 Michal Fedyna 11/14/2023 #2

我正在尝试重构我的钩子:

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),
},
}));

自:

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