You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为转换自定义主题样式的React Hook编写TypeScript类型

实现带主题映射与TypeScript类型约束的React Native useStyle Hook

需求说明

封装一个useStyle Hook,接收包含自定义主题键的样式对象,将主题键映射为实际主题值,最终返回符合StyleSheet.create()要求的样式对象,同时通过TypeScript保证输入输出的类型严格匹配。

示例效果:

function useStyle(style){
  const theme = useTheme();
  const newStyle = 转换主题键为实际值的逻辑
  
  return StyleSheet.create(newStyle)
}

// 输入:带主题键的样式
const style = {
  container: {
    margin: "small",
    backgroundColor: "background"
  },
  text: {
    color: "text",
    fontSize: "medium"
  }
}

const styleObject = useStyle(style)

// 输出类型自动推导为:
{
  container: {
    margin: number,
    backgroundColor: string
  },
  text: {
    color: string,
    fontSize: number
  }
}

简化示例的类型报错问题

以下简化示例中,访问style.margin和style.fontSize时触发类型错误:

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 = {};

    // 类型报错:类型“ViewStyle | TextStyle”上不存在属性“margin”
    if (style.margin) {
      newStyle.margin = theme.spacing[style.margin]
    }

    // 类型报错:类型“ViewStyle | TextStyle”上不存在属性“fontSize”
    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<T>中每个属性是ViewStyle | TextStyle的联合类型,TypeScript无法确定当前遍历的style具体属于哪一种类型,因此只允许访问联合类型的共有属性,而margin和fontSize是各自类型独有的属性,触发类型错误。

简化示例修复方案

通过类型守卫区分ViewStyle和TextStyle,让TypeScript明确当前样式的类型:

// 新增类型守卫函数
const isViewStyle = (style: ViewStyle | TextStyle): style is ViewStyle => {
  return 'margin' in style;
};

const isTextStyle = (style: ViewStyle | TextStyle): style is TextStyle => {
  return 'fontSize' in style;
};

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 (isViewStyle(style) && style.margin) {
      newStyle.margin = theme.spacing[style.margin];
    }

    if (isTextStyle(style) && style.fontSize) {
      newStyle.fontSize = theme.font.size[style.fontSize];
    }

    newStyles[key] = newStyle;
  }

  return newStyles;
}

完整React Native实现

主题与样式类型定义

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; // 行动召唤色
  toned: string; // 灰色系
  success: string; // 绿色
  warning: string; // 黄色
  error: string; // 红色
  info: string; // 蓝色
  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;

// 自定义样式类型(带主题键)
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;
};

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

类型映射工具与useStyles Hook实现

import { useTheme, StyleSheet, useMemo } from 'react-native';
import NamedStyles = StyleSheet.NamedStyles;

// 定义输入样式类型
export type InputStyles<T> = {
  [P in keyof T]: ViewStyle | TextStyle | ImageStyle;
};

// 定义转换后的输出样式类型:将主题键替换为实际值类型
type TranslatedViewStyle = Omit<ViewStyle, keyof SpacingStyle | 'backgroundColor' | 'borderColor'> & {
  [K in keyof SpacingStyle]: number | undefined;
} & {
  backgroundColor?: string;
  borderColor?: string;
};

type TranslatedTextStyle = Omit<TextStyle, keyof SpacingStyle | 'fontSize' | 'fontWeight' | 'fontColor'> & {
  [K in keyof SpacingStyle]: number | undefined;
} & {
  fontSize?: number;
  fontWeight?: FontWeight;
  fontColor?: string;
};

type TranslatedImageStyle = ImageStyle;

type TranslatedStyles<T> = {
  [P in keyof T]: 
    T[P] extends ViewStyle ? TranslatedViewStyle :
    T[P] extends TextStyle ? TranslatedTextStyle :
    TranslatedImageStyle;
};

// 样式转换工具函数
const translateStyle = <S extends ViewStyle | TextStyle | ImageStyle>(style: S, theme: Theme): 
  S extends ViewStyle ? TranslatedViewStyle :
  S extends TextStyle ? TranslatedTextStyle :
  TranslatedImageStyle => {
  const translated: Record<string, any> = {...style};

  // 处理间距样式
  const spacingProps: (keyof SpacingStyle)[] = [
    'margin', 'marginVertical', 'marginHorizontal', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
    'padding', 'paddingVertical', 'paddingHorizontal', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
    'gap', 'rowGap', 'columnGap'
  ];
  spacingProps.forEach(prop => {
    if (translated[prop]) {
      translated[prop] = theme.spacing[translated[prop] as KeyofSpacingTheme];
    }
  });

  // 处理ViewStyle专属属性
  if ('backgroundColor' in translated && translated.backgroundColor) {
    translated.backgroundColor = theme.colors[translated.backgroundColor as KeyofColorTheme];
  }
  if ('borderColor' in translated && translated.borderColor) {
    translated.borderColor = theme.colors[translated.borderColor as KeyofColorTheme];
  }

  // 处理TextStyle专属属性
  if ('fontSize' in translated && translated.fontSize) {
    translated.fontSize = theme.font.size[translated.fontSize as KeyofFontSizeTheme];
  }
  if ('fontWeight' in translated && translated.fontWeight) {
    translated.fontWeight = theme.font.weight[translated.fontWeight as KeyofFontWeightTheme];
  }
  if ('fontColor' in translated && translated.fontColor) {
    translated.fontColor = theme.colors[translated.fontColor as KeyofColorTheme];
  }

  return translated as any;
};

// 最终的useStyles Hook
export function useStyles<T extends InputStyles<T>>(styles: T): NamedStyles<TranslatedStyles<T>> {
  const theme = useTheme<Theme>();

  return useMemo(() => {
    const translatedStyles: Record<string, any> = {};
    Object.entries(styles).forEach(([key, style]) => {
      translatedStyles[key] = translateStyle(style, theme);
    });
    return StyleSheet.create(translatedStyles) as NamedStyles<TranslatedStyles<T>>;
  }, [styles, theme]);
}

使用示例

import { View, Text } from 'react-native';
import { useStyles } from './path-to-useStyles';

const ExampleComponent = () => {
  const styles = useStyles({
    container: {
      margin: 'medium',
      backgroundColor: 'background',
      justify: 'center',
      align: 'center',
      flex: 1
    },
    title: {
      fontSize: 'large',
      fontWeight: 'bold',
      fontColor: 'primary',
      marginBottom: 'small'
    }
  });

  return (
    <View style={styles.container}>
      <Text style={styles.title}>示例文本</Text>
    </View>
  );
};

内容的提问来源于stack exchange,提问作者Michal Fedyna

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 09:20:54