扩展 TypeScript 中字典值的可能类型

Extend the possible types of values of dictionary in TypeScript

我在 TypeScript 中的 React Native 项目中有一个强类型样式对象:

const styles = StyleSheet.create({
  container:{
    backgroundColor: 'red',
    flex: 1
  },
});

这正常工作,因为 StyleSheet.create 期望任意命名的键具有 ViewStyleTextStyleImageStyle 的目标值,定义如下:

export namespace StyleSheet {
    type NamedStyles<T> = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle };

    /**
     * Creates a StyleSheet style reference from the given object.
     */
    export function create<T extends NamedStyles<T> | NamedStyles<any>>(styles: T | NamedStyles<T>): T;
    [...]
}

container是任意键,backgroundColorViewStyle中定义了一个ColorValue类型的值,定义为string | OpaqueColorValue 其中 OpaqueColorValue 是唯一符号。

但是,我正在构建一个函数,我想在以前以原始类型接受 ColorValue 的地方使用以下类型签名:

ColorValue | { light: ColorValue, dark: ColorValue }

连同一个名为 lightdark 的参数提供给我的函数,我将像这样使用它:

const styles = myFunction({
  container:{
    backgroundColor: {
      light: 'red',
      dark: 'blue'
    },
    flex: 1
  },
}, 'dark');

它会选择合适的密钥,例如它将 return:

{
    container:{
      backgroundColor: 'blue' //as we picked the 'dark' key from original input
      flex: 1
    }
}

用简单的英语来说,我希望我的函数的输入类型接受 StyleSheet.create 接受的任何内容,并且 除了 之外,还接受任何地方的 {light: ColorValue, dark: ColorValue}它接受了 ColorValue。然后它会选择适当的键,然后在内部调用 StyleSheet.create 并 return 得到结果。

我写了这个函数并且它可以工作,但我正在努力让它在 IntelliSense 和 linter 中接受带有 light/dark 键的对象(当然,没有强制转换为 anyunknown).

如何实现?

我想你可能会受益于一些像这样的conditional, mapped types

type DeepReplaceSupertype<T, S, D> = [S] extends [T] ? D : {
    [K in keyof T]: DeepReplaceSupertype<T[K], S, D>
}

type DeepReplaceSubtype<T, S, D> = [T] extends [S] ? D : {
    [K in keyof T]: DeepReplaceSubtype<T[K], S, D>
}

想法是 DeepReplaceSupertype<T, S, D> 采用类型 T、源类型 S 和目标类型 D 并检查:如果 S 可分配给 T,然后将其替换为 D。否则,递归遍历 T 并以相同方式替换所有属性和子属性。 DeepReplaceSubtype<T, S, D> 类似,但它检查 T 是否可分配给 S.

那么您的函数签名可能如下所示:

interface Shade { light: ColorValue, dark: ColorValue }

export declare function myFunction<
    T extends DeepReplaceSupertype<NamedStyles<any>, ColorValue, Shade>>(
        styles: T, shade: 'light' | 'dark'
    ): DeepReplaceSubtype<T, Shade, ColorValue>;

含义:接受任何 T 类型的内容,其中您接受 NameStyles 并替换接受 ColorValue 的子属性并将它们更改为 Shade。然后,return 值应采用 T 并替换任何属于 Shade 子类型的子属性,并将它们变回 ColorValue。它应该像这样工作:

const styles = StyleSheet.myFunction({
    container: {
        backgroundColor: {
            light: 'red',
            dark: 'blue'
        },
        flex: 1
    },
}, 'dark');

/* const styles: {
    container: {
        backgroundColor: ColorValue;
        flex: number;
    };
} */

看起来像你想要的,我想。你可能会变得更聪明,让输出类型具体是从 lightdark 中选择的值的类型(所以 string 而不是 ColorValue),但这会更复杂,而且可能没有必要,所以我就到此为止。

Playground link to code