提问人:Michal Fedyna 提问时间:11/13/2023 更新时间:11/14/2023 访问量:66
如何键入一个接受接口并转换为另一个接口的函数?
How to type a function which takes interface and translates to another one?
问:
我正在尝试制作一个包装 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.margin
style.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]);
}
答:
在我的项目中,我也试图强制执行类型。然后,在意识到我只为自己创造了一个巨大的样板之后,我决定放宽要求并寻求更简单的解决方案。我知道这不是一个完美的答案,但至少我可以分享一些经验。
我创建了这个:
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>
我不是在强制执行样式:我只是指示使用已经设置样式的自定义组件的“正确路径”。这使得代码读起来更干净(比 更好),而且您不必考虑正确的类型:只需正确使用它和拥有正确的组件即可。Subtitle
Text 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];
}
只有这样,才能尝试将它们组合成更大的类型,这样您就有正确的粒度来查看出了什么问题。
评论
<Select />
我正在尝试重构我的钩子:
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',
}});
评论
ViewStyle
TextStyle
type NamedStyles<T> = {[P in keyof T]: ViewStyle | TextStyle | ImageStyle};
{ margin: number, fontSize?: never} | { margin?: never, fontSize: number}
if (style.margin) { } if (style.fontSize) { }
if (style.margin) { } else if (style.fontSize) { }
else
container
title
style