我已经为我的Reaction Native应用程序创建了一个函数,它接受一个StyleSheet.NamedStyles<T> Like对象,但每个样式也可以有一个可选的dark对象,这就是另一种样式本身.该函数返回一个钩子,如果我的应用程序主题是深色的,则该钩子将深色样式合并到基本样式中,并返回它(如果是浅色主题,则只返回基本样式).

以下是一些模拟类型的源代码,仅举个例子.如果这是可能的,我怎么才能让它工作呢?

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

推荐答案

我在这里只关注类型;我假设您的实现在运行时的行为是可以接受的(或者至少超出了问题的范围).让我们给dynamicStyleSheet()提供以下调用签名:

declare function dynamicStyleSheet<
    const T extends Record<keyof T, Style & { dark?: Style }>>(
        styles: T
    ): () => { [K in keyof T]:
        MergeStyle<T[K], T[K]["dark"] & object>
    }

type Style = ViewStyle | TextStyle | ImageStyle;

因此,styles的类型是从constrainedRecord<keyof T, Style & { dark?: Style }>generic类型参数T,这意味着每个属性都是Style类型,还有一个可选的dark属性也是Style类型.我相信,这捕获了您对输入类型的一般意图.

请注意,Tconst type parameter,这意味着当您调用dynamicStyleSheet({⋯})时,编译器将尽可能精确地推断传递给它的object literal的类型,类似于使用const assertion调用dynamicStyleSheet({⋯} as const)的方式.因此,它将跟踪准确的literal types个属性,如"white""black",而不仅仅是string…您说这对您的用例很重要.

dynamicStyleSheet的返回类型是返回mapped type {[K in keyof T]: MergeStyle<T[K], T[K]["dark"] & object>}的函数,其中T[K]["dark"] & object是"暗"样式类型(intersected,其中object确保从darkoptional的任何undefined被移除,而从dark为缺失的任何unknownobject替换.

每个属性将从T[K]个转换为MergeStyle<T[K], T[K]["dark"] & object>>个.所以现在我们需要定义MergeStyle<T, D>,这样它的行为就像你想要的那样.


以下是一种方法:

type MergeStyle<T extends Style, D extends object> =
    { [K in Exclude<keyof T | keyof D, "dark">]:
        (K extends keyof T ? T[K] : undefined) |
        (K extends keyof D ? D[K] : never)
    } & {};

这是一个mapped type,我们迭代T中的所有属性和D中的所有属性,但"dark"除外.对于每个键K,如果它存在于T中,则我们包括它,否则我们包括undefined,以考虑可能不使用深色样式的事实.并且,对于每个键K,如果它存在于D中,则我们包括它,否则我们包括never,因为我们永远不会用undefined重写该属性.

哦,最后的& {}只是为了说服智能感知将扩展MergeStyle<X, Y>显示为完整的属性集,而不是只保留MergeStyle<X, Y>.


让我们来测试一下:

const useStyles = dynamicStyleSheet({
    example: {
        height: 50,
        color: "white",
        dark: {
            color: "black",
            width: 50
        }
    },
    example2: {
        textAlign: "center",
        dark: {}
    },
    example3: {
        dark: {
            fontStyle: "italic"
        }
    },
    example4: {
        borderStyle: "solid"
    }
})

产生:

const styles = useStyles();
/*
const styles: {
    readonly example: {
        height: 50;
        width: 50 | undefined;
        color: "white" | "black";
    };
    readonly example2: {
        textAlign: "center";
    };
    readonly example3: {
        fontStyle: "italic" | undefined;
    };
    readonly example4: {
        borderStyle: "solid";
    };
}
*/

看上go 不错.

Playground link to code

Typescript相关问答推荐

如何在函数参数中使用(或模仿)`success`的行为?

typescribe不能使用值来索引对象类型,该对象类型满足具有该值的另一个类型'

Angular 行为:属性类型从SignalFunction更改为布尔

Redux—Toolkit查询仅在不为空的情况下添加参数

与字符串文字模板的结果类型匹配的打字脚本

编剧错误:正在等待Expect(Locator).toBeVisible()

Express Yup显示所有字段的所有错误

打字类型保护递归判断

Cypress页面对象模型模式.扩展Elements属性

vue-tsc失败,错误引用项目可能无法禁用vue项目上的emit

刷新时保持当前名称的Angular

如何在Angular 服务中制作同步方法?

作为函数参数的联合类型的键

在单独的组件中定义React Router路由

如何创建允许使用带有联合类型参数的Array.from()的TS函数?

我应该使用服务方法(依赖于可观察的)在组件中进行计算吗?

带有 Select 器和映射器的Typescript 泛型

当受其他参数限制时,通用参数类型不会缩小

如何强制对象数组中的对象属性具有非空字符串值?

类型string不可分配给类型string| 联盟| 的'