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

如何合并泛型对象并实现完全类型安全?React Native动态样式场景

React Native 动态主题样式的类型安全实现问题

我为React Native应用编写了dynamicStyleSheet函数,它接收带可选dark子样式的样式对象,返回一个Hook——深色主题时合并基础样式与dark样式,浅色主题时仅返回基础样式。但当前类型推导不符合预期:

  • styles.example.color应推导为"white" | "black",实际是string
  • styles.example.height应推导为50,实际是number
  • styles.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 16:34:55