TypeScript中如何为混合类型配置对象设置正确索引类型
问题场景
我有一个用于配置Material-UI Drawer组件的对象,抽屉组件分为单个组件和分组组件两类。相关枚举、类型定义及配置对象如下:
枚举定义
export enum InsuranceTopicPrimaryKeys { POLICY_HOLDERS = "policyHolders", INSURANCE_PROVIDERS = "providers", INSURANCE_TYPE = "type", CLAIMS = "claims", } export enum InsuranceTopicSecondaryKeys { POLICY_MEMBERS = "policyMembers", } export type InsuranceTopicKeys = | InsuranceTopicPrimaryKeys | InsuranceTopicSecondaryKeys; export enum SectionKeys { CLAIMANTS = "claimants", FULFILLED_CLAIMS = "fullfilledClaims", UNFULFILLED_CLAIMS = "unfullfilledClaims", }
配置对象及类型
export interface BaseTopicConfig { key: InsuranceTopicKeys; title: string; secondaryKey?: InsuranceTopicSecondaryKeys; secondaryTopic?: SecondaryTopicConfiguration; } export interface TopicConfiguration extends BaseTopicConfig { topicSelector: TopicSelector; component: TopicComponent; } export interface SecondaryTopicConfiguration extends TopicConfiguration { primaryKey: InsuranceTopicPrimaryKeys; } export interface TopicConfigurationGrouped extends BaseTopicConfig { topicSelectorCreator: TopicSelectorCreator; component: TopicComponentGroup; sections: SectionKeys[]; } export const TOPICS = { [InsuranceTopicPrimaryKeys.POLICY_HOLDERS]: { key: InsuranceTopicPrimaryKeys.POLICY_HOLDERS, secondaryKey: InsuranceTopicSecondaryKeys.POLICY_MEMBERS, title: "Policy Holders", component: (props: TopicDrawerProps) => ( <TopicDrawer hasSecondary={true} {...props} /> ), topicSelector: keyedTopicSelector.showAll, secondaryTopic: { key: InsuranceTopicSecondaryKeys.POLICY_MEMBERS, primaryKey: InsuranceTopicPrimaryKeys.POLICY_HOLDERS, title: "Policy Members", component: (props: TopicDrawerProps) => ( <TopicDrawer isSecondary={true} {...props} /> ), topicSelector: keyedTopicSelector.showRelated, }, }, [InsuranceTopicPrimaryKeys.INSURANCE_PROVIDERS]: { key: InsuranceTopicPrimaryKeys.INSURANCE_PROVIDERS, title: "Policy Holders", component: TopicDrawerSimple, topicSelector: keyedTopicSelector.showAll, }, [InsuranceTopicPrimaryKeys.INSURANCE_TYPE]: { key: InsuranceTopicPrimaryKeys.INSURANCE_PROVIDERS, title: "Policy Holders", component: TopicDrawerSimple, topicSelector: keyedTopicSelector.showAll, }, [InsuranceTopicPrimaryKeys.CLAIMS]: { key: InsuranceTopicPrimaryKeys.CLAIMS, title: "Claims", component: TopicDrawerGroup, topicSelectorCreator: createMultiKeyedTopicSelector, sections: [ SectionKeys.CLAIMANTS, SectionKeys.FULFILLED_CLAIMS, SectionKeys.UNFULFILLED_CLAIMS, ], }, }; export type SectionConfig = { key: SectionKeys; title: string; }; export const SECTIONS: Record<SectionKeys, SectionConfig> = { [SectionKeys.CLAIMANTS]: { key: SectionKeys.CLAIMANTS, title: "Claimants", }, [SectionKeys.FULFILLED_CLAIMS]: { key: SectionKeys.FULFILLED_CLAIMS, title: "Fulfilled Claims", }, [SectionKeys.UNFULFILLED_CLAIMS]: { key: SectionKeys.UNFULFILLED_CLAIMS, title: "Unfulfilled Claims", }, };
尝试的方案及错误
为了避免每次查询InsuranceTopicPrimaryKeys.CLAIMS时手动断言类型,我尝试定义键子集并设置TOPICS的类型为联合Record:
// subset that match config of type TopicConfigurationGrouped export type TopicGroups = InsuranceTopicPrimaryKeys.CLAIMS const TOPICS: Record<InsuranceTopicPrimaryKeys, TopicConfiguration> | Record<TopicGroups, TopicConfigurationGrouped>
但在使用辅助函数时出现错误:
const isPrimaryTopic = ( topicType: string ): topicType is InsuranceTopicPrimaryKeys => Object.values(InsuranceTopicPrimaryKeys).includes( topicType as InsuranceTopicPrimaryKeys ); const getConfig = ( configKey: string ): | SecondaryTopicConfiguration | TopicConfiguration | TopicConfigurationGrouped | undefined => isPrimaryTopic(configKey) ? TOPICS[configKey as InsuranceTopicPrimaryKeys] : Object.values(TOPICS).find( (topicConfig) => topicConfig?.secondaryKey === configKey )?.secondaryTopic;
错误信息:
Element implicitly has an 'any' type because expression of type 'InsuranceTopicPrimaryKeys' can't be used to index type 'Record<InsuranceTopicPrimaryKeys, TopicConfiguration> | Record<InsuranceTopicPrimaryKeys.CLAIMS, TopicConfigurationGrouped>'.
Property '[InsuranceTopicPrimaryKeys.POLICY_HOLDERS]' does not exist on type 'Record<InsuranceTopicPrimaryKeys, TopicConfiguration> | Record<InsuranceTopicPrimaryKeys.CLAIMS, TopicConfigurationGrouped>'.(7053)
解决方案
问题出在联合Record类型无法让TypeScript确定所有主键都能安全索引,因为第二个Record只包含CLAIMS键,其他主键在该类型中不存在。我们需要为TOPICS定义一个精确的映射类型,让每个主键对应其具体的配置类型。
步骤1:定义精确的映射类型
创建一个类型,为每个InsuranceTopicPrimaryKeys成员指定对应的配置类型:
// 可选:先定义分组键的集合,便于后续维护 type GroupedTopicKeys = InsuranceTopicPrimaryKeys.CLAIMS; // 自动生成映射类型:分组键对应TopicConfigurationGrouped,其余对应TopicConfiguration type TopicConfigMap = { [K in InsuranceTopicPrimaryKeys]: K extends GroupedTopicKeys ? TopicConfigurationGrouped : TopicConfiguration; };
步骤2:为TOPICS指定映射类型
将TOPICS的类型设置为TopicConfigMap,TypeScript会自动验证配置是否符合类型要求,同时精确推断每个键的返回类型:
export const TOPICS: TopicConfigMap = { // 原配置内容保持不变 };
现在访问TOPICS[InsuranceTopicPrimaryKeys.CLAIMS]时,TypeScript会自动推断它是TopicConfigurationGrouped类型,无需手动断言;访问其他主键则自动推断为TopicConfiguration类型。
步骤3:修正辅助函数
调整getConfig函数的类型,让它适配新的映射类型:
const getConfig = ( configKey: string ): SecondaryTopicConfiguration | TopicConfigMap[InsuranceTopicPrimaryKeys] | undefined => { if (isPrimaryTopic(configKey)) { // 用keyof TopicConfigMap确保索引安全 return TOPICS[configKey as keyof TopicConfigMap]; } return Object.values(TOPICS).find( (topicConfig) => topicConfig?.secondaryKey === configKey )?.secondaryTopic; };
内容的提问来源于stack exchange,提问作者AncientSwordRage

