Typescript:字典类型,其键属于嵌套类型

kh212irz  于 2023-03-13  发布在  TypeScript
关注(0)|答案(1)|浏览(131)

背景

我在想,是否有可能在TypeScript中创建一个类型,允许安全地将配置与允许的值进行Map。
假设我有下面的接口,它作为某个配置节点的定义:

interface CategoryDefinition {
    property1: string,
    actions: { [key: string]: string }
}

以及实现该接口的实际配置(通常可以是从文件加载的JSON,该示例只是为了更好地说明问题):

const definitions: { [key: string]: CategoryDefinition } = {
    key1: {
        property1: "PROPERTY_NAME_1",
        actions: {
            action1: "ACTION_1",
            action2: "ACTION_2"
        }
    },
    key2: {
        property1: "PROPERTY_NAME_2",
        actions: {
            action3: "ACTION_3",
            action4: "ACTION_4"
        }
    },
    // etc...
};

配置被Map到内部具有其他方法的对象,因此CategoryDefinition中的每个action都有一个来自父对象的property1,如下所示:

class ConfigValue {
    private propertyName: string;
    private action: string;

    constructor(category: string, action: string) {
        this.category = category;
        this.action = action;
    }

    methondOne(): void { /* doesn't really matter what's inside */}
}

问题

我想在配置对象/ hashmap中保留这样的Map对象,该对象具有来自上一示例的definitions的键,并且每个对象都具有来自嵌套在其中的Category.action对象的键。

export const configMapped /*: TypeImLookingFor */ = {
    key1: {
        action1: ConfigValue("PROPERTY_NAME_1", "ACTION_1"),
        action2: ConfigValue("PROPERTY_NAME_1", "ACTION_2"),
    },
    key2: {
        action3: ConfigValue("PROPERTY_NAME_2", "ACTION_3"),
        action4: ConfigValue("PROPERTY_NAME_2", "ACTION_4"),
    },
}

**我应该使用什么类型来确保这些actions属于在上一步中定义的有限有效键集?**我的目标是按以下方式使用此配置:

// valid call, `action1` is valid key for `key1`
configMapped.key1.action1.methodOne();

// invalid call
configMapped.key1.action4;

这在TypeScript中有可能吗?它不一定是复杂类型,它可以是相互关联的不同类型的集合。
感谢您的帮助和宝贵意见!

vxf3dgd4

vxf3dgd41#

你应该能够使用Map类型来获得你想要的行为。下面是我如何编写Map的:

type MapConfig<T extends Record<keyof T, CategoryDefinition>> =
  { [K in keyof T]: { [P in keyof T[K]['actions']]: ConfigValue } };

然后,对于一个 * 足够具体的 * definitions,你可以写

type MappedDefinitions = MapConfig<typeof definitions>.

这可能是答案的结尾,但我在评论中提到了一个问题,你 * 不能 * 做你在这里做的事情:

const badDefinitions:  { [key: string]: CategoryDefinition } = { /* config */ }

如果你显式地将definitions变量注解为{ [key: string]: CategoryDefinition }类型,那么你实际上已经丢弃了编译器可能拥有的关于分配给它的特定键和子键的任何信息,编译器说:“好吧,我所知道的关于badDefinitions的所有信息就是它的所有属性都是CategoryDefinition”。
如果尝试将MapConfig应用于 that,丢失的信息仍然会丢失,并且您会得到可能的最通用类型:ConfigValue s的未指定字典的未指定字典:

type BadMappedDefinitions = MapConfig<typeof badDefinitions>;
/* type BadMappedDefinitions = { [x: string]: { [x: string]: ConfigValue; }; } */

所以你需要备份并让编译器推断definitions的类型。如果你是用你问题中的玩具例子做的,这是没有问题的,只要留下注解:

const definitions = { /* same as yours */ }

或者,如果您最终关心的是属性的确切字符串值,则可能使用constAssert(在本问题中您不会关心,但在真实的代码中可能会关心):

const definitions = { /* same as yours */ } as const;

如果从静态JSON文件导入,可以使用--resolveJsonModule,但不能使用as const(请参见microsoft/TypeScript#32063了解其特性请求)。
如果在代码编译之后通过fetch加载它,则什么也做不了;编译器在你的JS运行的时候已经过时了,除非你事先知道确切的键和属性,否则你不能使用类型注解,如果你知道的话,你还不如使用静态资源,而不是获取任何东西。
假设这只是内联代码,我们让编译器推断definitions的类型,然后您可以验证MappedDefinitions是否计算为所需的类型:

/*
type MappedDefinitions = {
    key1: {
        action1: ConfigValue;
        action2: ConfigValue;
    };
    key2: {
        action3: ConfigValue;
        action4: ConfigValue;
    };
}
*/

并使用它,

const configMapped: MappedDefinitions = { /* same as yours */ };

为您提供基于键名允许/禁止属性访问所需的类型信息:

configMapped.key1.action1.methodOne(); // okay
configMapped.key1.action4; // error! action4 does not exist

Playground链接

相关问题