如何合并泛型对象并实现完全类型安全?React Native动态样式场景
React Native 动态主题样式的类型安全实现问题
我为React Native应用编写了dynamicStyleSheet函数,它接收带可选dark子样式的样式对象,返回一个Hook——深色主题时合并基础样式与dark样式,浅色主题时仅返回基础样式。但当前类型推导不符合预期:
styles.example.color应推导为"white" | "black",实际是stringstyles.example.height应推导为50,实际是numberstyles.example.width应推导为50 | undefined,实际是number | undefined
当前代码如下:
// Example: const useStyles = dynamicStyleSheet({ example: { height: 50, color: "white", dark: { color: "black", width: 50 } } }) // ts might say useStyles is a hook and cannot be called here. it doesn't matter, the type matters. const styles = useStyles() type color = typeof styles.example.color // should be "white" | "black". but it's string type height = typeof styles.example.height // should be 50. but it's number type width = typeof styles.example.width // should be 50 | undefined. but it's number | undefined type ImageStyle = { height?: number width?: number objectFit?: "cover" | "contain" | "fill" | "scale-down" } type TextStyle = { fontSize?: number color?: string fontStyle?: "normal" | "italic" textAlign?: "auto" | "left" | "right" | "center" | "justify" } type ViewStyle = { height?: number width?: number borderStyle?: "solid" | "dotted" | "dashed" } type DynamicNamedStyles<T> = { [P in keyof T]: (ViewStyle | TextStyle | ImageStyle) & { dark?: ViewStyle | TextStyle | ImageStyle } } type MergedStyles<T extends DynamicNamedStyles<T>> = { [P in keyof T]: Omit<T[P], "dark"> & (T[P]["dark"] extends infer D ? Partial<D> : object) } type OmitDark<T> = Omit<T, "dark"> type FinalStyles<T extends DynamicNamedStyles<T>> = { [P in keyof T]: OmitDark<MergedStyles<T>[P]> } export function dynamicStyleSheet<T extends DynamicNamedStyles<T> | DynamicNamedStyles<any>>( styles: T & DynamicNamedStyles<any> ) { return function useStyles() { const isDark = useIsDarkScheme() const lightStyles = objectMap(styles, ({ dark, ...style }) => ({ ...style })) as FinalStyles<T> const darkStyles = objectMap(styles, ({ dark, ...style }) => ({ ...style, ...(dark ?? {}) })) as FinalStyles<T> return isDark ? darkStyles : lightStyles } } const useIsDarkScheme = () => true // mock type Dictionary<T> = { [key: string]: T } export function objectMap<TValue, TResult>( obj: Dictionary<TValue>, valSelector: (val: TValue, obj: Dictionary<TValue>) => TResult, keySelector?: (key: string, obj: Dictionary<TValue>) => string, ctx?: Dictionary<TValue> ) { const ret = {} as Dictionary<TResult> for (const key of Object.keys(obj)) { const retKey = keySelector ? keySelector.call(ctx ?? null, key, obj) : key const retVal = valSelector.call(ctx ?? null, obj[key], obj) ret[retKey] = retVal } return ret }
问题根源
原类型定义中使用了宽泛的联合类型(ViewStyle|TextStyle|ImageStyle),导致TypeScript自动将字面量类型(如"white"、50)拓宽为基础类型(string、number),同时无法准确合并基础样式与dark样式的属性类型。
解决方案
通过调整类型定义,让TypeScript保留字面量类型,并精准合并基础样式与dark样式的属性类型:
1. 修正动态样式的类型约束
重新定义DynamicStyle,让dark样式与基础样式类型保持兼容,避免宽泛的联合类型:
type ImageStyle = { height?: number width?: number objectFit?: "cover" | "contain" | "fill" | "scale-down" } type TextStyle = { fontSize?: number color?: string fontStyle?: "normal" | "italic" textAlign?: "auto" | "left" | "right" | "center" | "justify" } type ViewStyle = { height?: number width?: number borderStyle?: "solid" | "dotted" | "dashed" } // 单个动态样式:基础样式 + 可选的同类型dark子样式 type DynamicStyle<S extends ViewStyle | TextStyle | ImageStyle> = S & { dark?: Partial<S> } // 命名动态样式集合 type DynamicNamedStyles<T> = { [P in keyof T]: DynamicStyle<T[P]> }
2. 定义精准的样式合并类型
编写MergeDarkStyle工具类型,逐个合并基础样式与dark样式的属性类型:
// 合并单个样式的基础属性与dark属性 type MergeDarkStyle<S extends DynamicStyle<any>> = { [K in keyof Omit<S, "dark">]: // 基础属性 + dark属性的联合类型(如果dark存在该属性) K extends keyof S["dark"] ? S[K] | S["dark"][K] : S[K] } & { // 处理dark中独有的属性(标记为可选) [K in Exclude<keyof NonNullable<S["dark"]>, keyof Omit<S, "dark">>]?: NonNullable<S["dark"]>[K] } // 最终的样式集合类型 type FinalStyles<T extends DynamicNamedStyles<T>> = { [P in keyof T]: MergeDarkStyle<T[P]> }
3. 调整函数泛型与类型断言
修正dynamicStyleSheet的泛型参数,确保TypeScript捕获完整的字面量类型:
export function dynamicStyleSheet<T extends Record<string, ViewStyle | TextStyle | ImageStyle>>( styles: DynamicNamedStyles<T> ) { return function useStyles() { const isDark = useIsDarkScheme() // 生成浅色主题样式 const lightStyles = Object.fromEntries( Object.entries(styles).map(([key, { dark, ...style }]) => [key, style]) ) as { [P in keyof T]: Omit<DynamicNamedStyles<T>[P], "dark"> } // 生成深色主题样式(合并基础与dark样式) const darkStyles = Object.fromEntries( Object.entries(styles).map(([key, { dark, ...style }]) => [ key, { ...style, ...dark } ]) ) as FinalStyles<DynamicNamedStyles<T>> return isDark ? darkStyles : lightStyles } }
4. (可选)修正自定义objectMap的类型
如果继续使用objectMap,调整其泛型定义以支持精准类型推导:
type Dictionary<T> = { [key: string]: T } export function objectMap<T extends Dictionary<any>, TResult>( obj: T, valSelector: (val: T[keyof T], key: keyof T, obj: T) => TResult ): { [P in keyof T]: TResult } { const ret = {} as { [P in keyof T]: TResult } for (const key of Object.keys(obj) as Array<keyof T>) { ret[key] = valSelector(obj[key], key, obj) } return ret }
测试验证
修改后,示例代码的类型推导将完全符合预期:
const useStyles = dynamicStyleSheet({ example: { height: 50, color: "white", dark: { color: "black", width: 50 } } }) const styles = useStyles() type color = typeof styles.example.color // "white" | "black" ✅ type height = typeof styles.example.height // 50 ✅ type width = typeof styles.example.width // 50 | undefined ✅
内容的提问来源于stack exchange,提问作者Mohammadios
相关产品推荐
相关产品推荐

