skip to Main Content

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


  1. 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 of getConfig. The type TOPICS is a record with keys that belong to InsuranceTopicPrimaryKeys enum and its values can be either TopicConfiguration or TopicConfigurationGrouped. Therefore, when you try to access a value of TOPICS using an index of type string (returned by the configKey parameter of getConfig), TypeScript infers that the index is of type InsuranceTopicPrimaryKeys, which is not compatible with string. 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 of TopicConfiguration and TopicConfigurationGrouped only, since SecondaryTopicConfiguration extends TopicConfiguration. This way, TypeScript will be able to infer the correct type of the value returned by TOPICS when accessed using an index of type InsuranceTopicPrimaryKeys. Here’s an updated version of getConfig:

    const getConfig = (
      configKey: InsuranceTopicKeys
    ): TopicConfiguration | TopicConfigurationGrouped | undefined => {
      const topicConfig = TOPICS[configKey];
      if (topicConfig) {
        return topicConfig;
      }
      return Object.values(TOPICS).find(
        (topicConfig) => topicConfig?.secondaryKey === configKey
      )?.secondaryTopic;
    };
    

    In this version, configKey is typed as InsuranceTopicKeys, which is the union of InsuranceTopicPrimaryKeys and InsuranceTopicSecondaryKeys. This ensures that the index used to access TOPICS is of the correct type.

    Note that you don’t need to define a separate type TopicGroups to represent the keys that belong to a TopicConfigurationGrouped. Instead, you can use the Record utility type to create a type that maps the keys of TOPICS to their respective value types, and then use a conditional type to filter out the keys that don’t belong to a TopicConfigurationGrouped.
    Here’s an example:

    type GroupedTopicKeys = {
      [K in keyof typeof TOPICS]: TOPICS[K] extends TopicConfigurationGrouped ? K : never;
    }[keyof typeof TOPICS];
    
    const groupedKeys: GroupedTopicKeys[] = ["CLAIMS"];
    
    const groupedTopics: Record<GroupedTopicKeys, TopicConfigurationGrouped> = {
      [InsuranceTopicPrimaryKeys.CLAIMS]: {
        key: InsuranceTopicPrimaryKeys.CLAIMS,
        title: "Claims",
        component: TopicDrawerGroup,
        topicSelectorCreator: createMultiKeyedTopicSelector,
        sections: [
          SectionKeys.CLAIMANTS,
          SectionKeys.FULFILLED_CLAIMS,
          SectionKeys.UNFULFILLED_CLAIMS,
        ],
      },
    };
    

    In this example, the GroupedTopicKeys type is defined using a mapped type that filters out the keys of TOPICS that don’t belong to a TopicConfigurationGrouped. The resulting type is a union of the remaining keys. The groupedKeys variable is then defined as an array of the keys that belong to a TopicConfigurationGrouped. Finally, a groupedTopics variable is defined as a record with keys of type GroupedTopicKeys and values of type TopicConfigurationGrouped.

    Login or Signup to reply.
  2. The type of TOPICS_TWO shouldn’t be a union. Since InsuranceTopicPrimaryKeys includes TopicGroups, you should exclude TopicGroups first. Then you can intersect the record with a new record of TopicGroups.

    export const TOPICS_TWO: Record<
        Exclude<InsuranceTopicPrimaryKeys, TopicGroups>,
        TopicConfiguration
    > &
        Record<TopicGroups, TopicConfigurationGrouped> = {
    

    Playground

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search