Extract pieces from a union type that includes a discriminator which is itself a union

My current type declaration looks like this:


    enum Type {
      A = 'A',
      B = 'B',
      C = 'C'
    }

    type Union =
     | {
         type: Type.A | Type.B;
         key1: string
       }
     | {
         type: Type.C;
         key2: string
       }

    type EnumToUnionMap = {
      [T in Type]: {
        [k in keyof Extract<Union, {type: T}>]: string
      }
    }
  

The complication arises when I try to access EnumToUnionMap[Type.A], which results in never (in reality, it should be a generic key signature like [x: string]: string, however, due to the Extract<Union, {type: T}> returning a never type when T is either Type.A or Type.B). On the other hand, EnumToUnionMap[Type.C] provides the expected output.


    {
      type: Type.C,
      key2: string
    }
  

The reason behind this behavior is that the type in EnumToUnionMap[Type.A] is Type.A | Type.B, and Type.A != (Type.A | Type.B), causing them not to match, resulting in never.

In essence, what I need to achieve is something like this:


    type EnumToUnionMap = {
      [T in Type]: {
        [k in keyof Extract<Union, T in Union['type']>]: string
      }
    }
  

The Reason for This:

I am receiving a response from an alerts endpoint with the following format:


    {
      type: Type,
      key1: string,
      key2: string
    }
  

Alerts of both Type.A and Type.B provide key1, while those of Type.C provide key2. I need to map these keys in the response to column names in a grid, where some alert types share common keys but have different display names for the columns:


    const columnMap: EnumToUnionMap = {
      [Type.A]: {
        key1: 'Column name'
        // Multiple keys will actually exist in this object for a given `Type`,
        // making mapping between `Type -> column name` impossible.
      },
      [Type.B]: {
        key1: 'Different column name'
      },
      [Type.C]: {
        key2: 'Another column name'
      }
    }
  

This approach enables me to do something like the following:


    const toColumnText = (alert) => columnMap[alert.type]

    ...

    if (alert.type === Type.A) {
      const key1ColumnName = toColumnText(alert).key1 // typed as string
      const key2ColumnName = toColumnText(alert).key2 // Typescript warns of undefined key
    }  
  

Answer №1

It's clear that using the Extract utility type is not suitable in this scenario because the relationship between the members of the union Union and the candidate types {type: T} is not based on simple assignability. Instead, the goal here is to identify the member U of the union Union such that T extends U["type"]. To accomplish this, one might need to forego TypeScript's built-in utility types and handle the type manipulation manually.


An example definition for EnumToUnionMap could look like:

type EnumToUnionMap = { [T in Type]: Union extends infer U ? U extends { type: any } ? (
    T extends U["type"] ? { [K in keyof U as Exclude<K, "type">]: U[K] } : never
) : never : never }

While this may seem complex at first glance, it accomplishes the desired outcome. IntelliSense confirms its evaluation to:

/* type EnumToUnionMap = {
    A: { key1: string; };
    B: { key1: string; };
    C: { key2: string; };
} */

Which indicates success.


With our understanding of its functionality, we can now break down and analyze the definition further:

type EnumToUnionMap = { [T in Type]: Union extends infer U ? U extends { type: any } ? (
    T extends U["type"] ? { [K in keyof U as Exclude]: U[K] } : never
) : never : never }

Similar to your approach, we iterate over enum values such as Type.A, Type.B, and Type.C. For each enum value T, we aim to segment Union into its individual members and extract the relevant one. By employing conditional type inference (link), we store Union in a new type parameter U, allowing us to form a distributive conditional type where U represents separate union members of Union. Therefore, instances of U subsequently pertain to specific subsets of Union.

type EnumToUnionMap = { [T in Type]: Union extends infer U ? U extends { type: any } ? (
    T extends U["type"] ? { [K in keyof U as Exclude]: U[K] } : never
) : never : never }

To determine the appropriate union member U from Union, we assess the conditional type T extends U["type"]? The condition is met only when U's type property encompasses T. If T is Type.A and U is {type: Type.A | Type.B, ...}, then this holds true. Contrastingly, if T is Type.A and U is {type: Type.C, ...}, it results in failure. By returning 'never' upon false verification, only the intended member of Union gets processed.

type EnumToUnionMap = { [T in Type]: Union extends infer U ? U extends { type: any } ? (
    T extends U["type"] ? { [K in keyof U as Exclude]: U[K] } : never
) : never : never }

Once the correct union member U is identified, the focus shifts to excluding the unwanted 'type' property before returning U. While omitting via Omit utilizingthe Omit utility type may suffice, it tends to generate verbose IntelliSense outputs (e.g., Omit<{type: Type.A | Type.B; key1: string}, "type"> instead of {key1: string}). Hence, an alternative custom Omit type leveraging key remapping in mapped types is implemented.


In summary, the EnumToUnionMap type navigates through each Type member, isolates the matching Union member with a supertype of its type, and eliminates the 'type' attribute from said member.

Playground link for code reference

Answer №2

TypeScript includes a unique feature that allows types to be narrowed using the `&` operator, so I decided to attempt a solution that does not involve too many complex techniques. Here is the complete code snippet that can help you achieve the desired outcome. Pay attention to the type annotations in the `toColumnText` function, as they are crucial for this solution.

enum Kind {
  X = 'X',
  Y = 'Y',
  Z = 'Z'
}

type Combination =
 | {
     kind: Kind.X | Kind.Y;
     value1: string;
   }
 | {
     kind: Kind.Z;
     value2: string
   }

type SearchResultByKind<TSearch, K extends Kind> = TSearch extends { kind: infer InferredK }
  ? (InferredK extends K ? (TSearch & { kind: K }) : never)
  : never;

type MapOfEnumsToUnion = {
  [K in Kind]: {
    [k in Exclude<keyof SearchResultByKind<Combination, K>, 'kind'>]: SearchResultByKind<Combination, K>[k];
  }
};

const mapping: MapOfEnumsToUnion = {
  [Kind.X]: { value1: 'Example name' },
  [Kind.Y]: { value1: 'Another name' },
  [Kind.Z]: { value2: 'Yet another name' }
}

const convertToText = <K extends Kind>(item: { kind: K }): MapOfEnumsToUnion[K] => mapping[item.kind];

const itemObj: Combination = { kind: Kind.X, value1: 'example' };
if (itemObj.kind === Kind.X) {
  const value1Name = convertToText(itemObj).value1 // typed as string
  const value2Name = convertToText(itemObj).value2 // TypeScript flags undefined key
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Setting a value to a FormBuilder object in Angular 2 with Typescript

During my use of Reactive form Validation (Model driven validation), I encountered an issue with setting the value to the form object on a Dropdown change. Here is my Formgroup: studentModel: StudentModel; AMform: FormGroup; Name = new FormControl("", Va ...

Angular 5 experiencing issues with external navigation functionality

Currently, I am attempting to navigate outside of my application. I have experimented with using window.location.href, window.location.replace, among others. However, when I do so, it only appends the href to my domain "localhost:4200/". Is it possible th ...

Create a tuple type by mapping an object with generics

I have a specified object: config: { someKey: someString } My goal is to generate a tuple type based on that configuration. Here is an example: function createRouter< T extends Record<string, string> >(config: T) { type Router = { // ...

Incorporating a unique variant with Tailwind called "

I'm currently struggling with inputting the configuration file for Tailwind. Despite my efforts, I cannot seem to get it right. The code appears correct to me, but I am unsure of what mistake I might be making. Below is the snippet of my code: import ...

What is the reason behind being able to assign unidentified properties to a literal object in TypeScript?

type ExpectedType = Array<{ name: number, gender?: string }> function go1(p: ExpectedType) { } function f() { const a = [{name: 1, age: 2}] go1(a) // no error shown go1([{name: 1, age: 2}]) // error displayed ...

Tips for testing nested subscribe methods in Angular unit testing

FunctionToTest() { this.someService.method1().subscribe((response) => { if (response.Success) { this.someService.method2().subscribe((res) => { this.anotherService.method3(); }) } }); } Consider the following scenario. ...

What could be causing the CSS loader in webpack to malfunction?

I am currently working on implementing the toy example mentioned in the css-loader documentation which can be found at https://github.com/webpack-contrib/css-loader Additionally, I have also followed a basic guide that recommends similar steps: https://cs ...

What is the best way to create an interface tailored to a specific scenario using TypeScript?

In my TypeScript project without React, I am dealing with an interface structured like this: export interface LayerStyling<T> { attribute: string; value: AllowedTypes; type: LayerTypes; layout?: { icon: string }; state ...

The Nuxt Vuex authentication store seems to be having trouble updating my getters

Despite the store containing the data, my getters are not updating reactively. Take a look at my code below: function initialAuthState (): AuthState { return { token: undefined, currentUser: undefined, refreshToken: undefined } } export c ...

Utilizing emotion with MUI v5 for dynamic theming

After upgrading MUI from v4 to v5, I'm facing some difficulties grasping the concept of theming with the various solutions available. I find it challenging to determine when to use MUI theming/styling components and when to opt for emotion ones. Whil ...

Tips for removing a row from a DataGrid column with the click of a button

I am facing a challenge with my data table that contains users. I am trying to implement a delete button for each row, but it seems like the traditional React approach may not work in this situation. Currently, I am utilizing the DataGrid component in the ...

Extracting data from an action using NgRx8

Hey everyone, I'm looking for some guidance on how to destructure action type and props in an ngrx effect. I'm struggling with this and could use some help! This is my list of actions: export const addTab = createAction( '[SuperUserTabs ...

How to upload files from various input fields using Angular 7

Currently, I am working with Angular 7 and typescript and have a question regarding file uploads from multiple input fields in HTML. Here is an example of what I am trying to achieve: <input type="file" (change)="handleFileInput($event.target.files)"&g ...

Input a new function

Trying to properly type this incoming function prop in a React Hook Component. Currently, I have just used any which is not ideal as I am still learning TypeScript: const FeaturedCompanies = (findFeaturedCompanies: any) => { ... } This is the plain fun ...

Aurelia CLI encounters difficulty importing chart.js when using TypeScript

Currently, I am utilizing typescript with aurelia/aurelia-cli. After npm installing chart.js, I proceeded to add it to my aurelia.json file like so: "dependencies": [ ... { "name": "chartjs", "path": "../node_modules/chart.js/d ...

implement some level of control within the ngFor directive in Angular

For instance, let's say I have an ngfor loop: <ng-container *ngFor="let setting of settings | trackBy: trackById"> <button mat-button [matMenuTriggerFor]="menu">Menu</button> <mat-me ...

"Unexpected compatibility issues arise when using TypeScript with React, causing errors in props functionality

Just the other day, my TypeScript+React project was running smoothly. But now, without making any changes to the configurations, everything seems to be broken. Even rolling back to previous versions using Git or reinstalling packages with NPM does not solv ...

Having trouble triggering a change event within a React component?

I'm working on a straightforward react-component that consists of a form. Within this form, the user can search for other users. To ensure the form is valid, there needs to be between 3 and 6 users added. To achieve this, I've included a hidden ...

Typescript error points out that the property is not present on the specified type

Note! The issue has been somewhat resolved by using "theme: any" in the code below, but I am seeking a more effective solution. My front-end setup consists of React (v17.0.2) with material-ui (v5.0.0), and I keep encountering this error: The 'palet ...

What could be causing the malfunction of my Nextjs Route Interception Modal?

I'm currently exploring a different approach to integrating route interception into my Nextjs test application, loosely following this tutorial. Utilizing the Nextjs app router, I have successfully set up parallel routing and now aiming to incorporate ...