I have an enum of keys for a configuration object, that renders a Material-UI Drawer Component, where the component in the drawer either matches one single component or a group of components in sections per sub-topic:
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",
}
and the configuration object looks like this:
export const TOPICS: Record<
InsuranceTopicPrimaryKeys,
TopicConfiguration | TopicConfigurationGrouped
> = {
[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",
},
};
The configurations (TopicConfiguration
, SecondaryTopicConfiguration
and TopicConfigurationGrouped
) share a base type, that they extend:
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[];
}
However there’re times when I’d like to look up the config of InsuranceTopicPrimaryKeys.CLAIMS
, and shortcut having to ‘prove’ to TS that it’s config is only ever going to be of the type TopicConfigurationGrouped
. For legacy reasons I can’t split the config up, as well are situations where I want to access the shared keys.
So I was hoping I could create a subset of ‘keys’, and have the union of two records (see below), not a record with it’s values being a union of types (Record<InsuranceTopicPrimaryKeys, TopicConfiguration | TopicConfigurationGrouped>
.
// subset that match config of type TopicConfigurationGrouped
export type TopicGroups = InsuranceTopicPrimaryKeys.CLAIMS
And then make the type of TOPICS
:
const TOPICS: Record<InsuranceTopicPrimaryKeys, TopicConfiguration> | Record<TopicGroups, TopicConfigurationGrouped>
But now when I go to get my config with this helper function:
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;
I get this error:
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)
How can I configure the type in my configuration mapping object (TOPICS, and/or in the linked playground TOPICS_TWO) so that this helper function (and others) can infer that the InsuranceTopicPrimaryKeys
type can index the union of the different Records?
Additional type to make this a working example are in this typescript playground
2
Answers
The error message you are getting is because the type of
TOPICS
is not compatible with the union type you have defined as the return type ofgetConfig
. The typeTOPICS
is a record with keys that belong toInsuranceTopicPrimaryKeys
enum and its values can be eitherTopicConfiguration
orTopicConfigurationGrouped
. Therefore, when you try to access a value ofTOPICS
using an index of typestring
(returned by the configKey parameter ofgetConfig
), TypeScript infers that the index is of typeInsuranceTopicPrimaryKeys
, which is not compatible withstring
. This is because TypeScript is not able to determine if the string is actually one of the enum values or not.One way to fix this error is to change the return type of
getConfig
to be a union ofTopicConfiguration
andTopicConfigurationGrouped
only, sinceSecondaryTopicConfiguration
extendsTopicConfiguration
. This way, TypeScript will be able to infer the correct type of the value returned byTOPICS
when accessed using an index of typeInsuranceTopicPrimaryKeys
. Here’s an updated version ofgetConfig
:In this version,
configKey
is typed asInsuranceTopicKeys
, which is the union ofInsuranceTopicPrimaryKeys
andInsuranceTopicSecondaryKeys
. This ensures that the index used to accessTOPICS
is of the correct type.Note that you don’t need to define a separate type
TopicGroups
to represent the keys that belong to aTopicConfigurationGrouped
. Instead, you can use theRecord
utility type to create a type that maps the keys ofTOPICS
to their respective value types, and then use a conditional type to filter out the keys that don’t belong to aTopicConfigurationGrouped
.Here’s an example:
In this example, the
GroupedTopicKeys
type is defined using a mapped type that filters out the keys ofTOPICS
that don’t belong to aTopicConfigurationGrouped
. The resulting type is a union of the remaining keys. ThegroupedKeys
variable is then defined as an array of the keys that belong to aTopicConfigurationGrouped
. Finally, agroupedTopics
variable is defined as a record with keys of typeGroupedTopicKeys
and values of typeTopicConfigurationGrouped
.The type of
TOPICS_TWO
shouldn’t be a union. SinceInsuranceTopicPrimaryKeys
includesTopicGroups
, you should excludeTopicGroups
first. Then you can intersect the record with a new record ofTopicGroups
.Playground