Specialized type for extra restriction on enum matching

In my current project, I am dealing with two enums named SourceEnum and TargetEnum. Each enum has a corresponding function that is called with specific parameters based on the enum value. The expected parameter types are defined by the type mappings SourceParams and TargetParams.

enum SourceEnum {
  SOURCE_A = 'SOURCE_A',
  SOURCE_B = 'SOURCE_B'
}

enum TargetEnum {
  TARGET_A = 'TARGET_A',
  TARGET_B = 'TARGET_B',
}

interface SourceParams {
  [SourceEnum.SOURCE_A]: { paramA: string };
  [SourceEnum.SOURCE_B]: { paramB: number };
}

interface TargetParams {
  [TargetEnum.TARGET_A]: { paramA: string };
  [TargetEnum.TARGET_B]: { paramB: number };
}

function sourceFn<S extends SourceEnum>(source: S, params: SourceParams[S]) { /* ... */ }

function targetFn<T extends TargetEnum>(target: T, params: TargetParams[T]) { /* ... */ }

I have created a mapping that involves functionality to evaluate a target value for each source value. My goal is to ensure that the params object used in calling sourceFn(x, params) will also be suitable for the call targetFn(mapping[x](), params). To address this requirement, I formulated the following type:

type ConstrainedMapping = {
  [K in SourceEnum]: <T extends TargetEnum>() => (SourceParams[K] extends TargetParams[T] ? T : never) 
};

const mapping: ConstrainedMapping = {
  [SourceEnum.SOURCE_A]: () => TargetEnum.TARGET_A;
  // ...
}

However, setting up the mapping as shown above triggers the following error message:

Type 'TargetEnum.TARGET_A' is not assignable to type '{ paramA: string; } extends TargetParams[T] ? T : never'.

Although my typing seems correct, I cannot grasp why this issue is arising. It appears that TypeScript may struggle with pinpointing the exact enum value at a certain point.

Is there a methodology to accomplish this? I am currently using TypeScript 4.2, but I experimented with versions 4.3 and 4.4-beta as well, all yielding the same outcome. Any solution applicable to 4.2 would be greatly appreciated, although I am open to implementing a future version if necessary.

Answer №1

Here, the expectation is for Typescript to analyze

<T extends TargetEnum>(): SourceParams[K] extends TargetParams[T] ? T : never;
, where all possible values of T are evaluated and a union is created based on the true conditions.

However, Typescript does not operate in this manner. It treats T as unknown and stops evaluating after replacing { paramA: string; } for SourceParams[K]. The desired distribution only happens with Distributive Conditional Types, so it's necessary to modify the ConstrainedMapping accordingly.

A Distributive Conditional Type is an alias where none of the parameters are restricted. Therefore, for a proper return value declaration, it must have its own separate type definition instead of being embedded within ConstrainedMapping.

type TargetWithMatchingParams<S, T> =
  S extends SourceEnum
    ? T extends TargetEnum
      ? SourceParams[S] extends TargetParams[T]
        ? T
        : never
      : never
    : never;

Since S and T cannot be constrained, more conditions need to be included in the template. Furthermore, the entire TargetEnum cannot be hardcoded directly; it must distribute across an unconstrained parameter in the type alias.

Following this modification, it can now be utilized within ConstrainedMapping:

type ConstrainedMapping = {
  [S in SourceEnum]: () => TargetWithMatchingParams<S, TargetEnum>;
};

It is important to note that the function is no longer generic—a crucial aspect for resolving the issue at hand—and the distribution expected earlier is achieved by passing TargetEnum into TargetWithMatchingParams.

If the scenario is static like the given example, removing () => from the ConstrainedMapping definition and using mapping[x] instead of mapping[x]() could enhance performance and readability.

In conclusion, there are certain limitations associated with this approach:

  1. When using mapping on a generic variable extending SourceEnum, it may cause issues. While

    targetFn(mapping[SourceEnum.SOURCE_A](), { paramA: 'foo' })
    functions correctly, TypeScript may encounter errors when handling calls such as:

    function bar<S extends SourceEnum>(src: S, params: SourceParams[S]) {
      targetFn(mapping[src](), params);
                               ^^^^^^
    //                         Argument of type 'SourceParams[S]'
    //                           is not assignable to parameter of type
    //                           'TargetParams[ConstrainedMapping[S]]'.
    }
    

    This highlights TypeScript's inability to fully understand the relationship between SourceParams and TargetParams, resulting in difficulty recognizing valid matches.

  2. In scenarios involving unions of sources and source parameters, unsafe values might not trigger errors. For instance:

    function baz(src: SourceEnum, params: SourceParams[SourceEnum]) {
      targetFn(mapping(src), params);
    }
    baz(SourceEnum.SOURCE_A, { paramB: 42 });
    

    Even though SOURCE_A and paramB create an invalid combination, TypeScript may not flag this discrepancy due to how unions function.

In summary, ensure that specific enum types are used while calling sourceFn and

targetFn</code, rather than relying on the entire enum. TypeScript lacks the capability to track relationships across distinct variables, so pairing an unknown <code>SourceEnum
with corresponding
SourceParams[SourceEnum]</code may go unchecked, irrespective of any definitions within <code>mapping
.

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

Utilizing req.session in an Express application with Angular (written in TypeScript) when deploying the backend and frontend separately on Heroku

I'm currently facing an issue where I am unable to access req.session from my Express app in Angular. Both the backend and frontend are deployed separately on Heroku. I have already configured CORS to handle HTTP requests from Angular to my Express ap ...

Experiencing CORS problem in Ionic 3 when accessing API on device

I am a newcomer to IONIC and I am utilizing a slim REST API with Ionic 3. Currently, I am encountering the following error: "Failed to load : Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin&apos ...

Having trouble displaying a "SectionList" in "React Native", it's just not cooperating

As a newcomer to programming, I recently started working with React Native. I attempted to create a FlatList, which was successful, but the data did not display as I intended. I realized I needed a header to organize the data the way I wanted, so I discove ...

I encountered an issue while making customizations to my default next.config.js file. Despite attempting various solutions, I consistently encountered an error regarding the invalid src property

I'm currently trying to introduce some custom configurations into the next.config.js file. However, I keep encountering an error regarding an invalid src prop. Despite my attempts to troubleshoot in various ways, the error persists. // ...

Timeout with Promise

I'm looking to enhance my understanding of working with promises by rewriting this function to resolve the promise instead of resorting to calling the callback function. export const connect = (callback: CallableFunction|void): void => { LOG.d ...

What causes the select dropdown to display an empty default in Angular 8 following an HTTP request?

I have created a simple HTML code to populate array elements in a dropdown list, with the default value being fetched from an HTTP service during the ngOnInit lifecycle hook. However, I am encountering an issue where the default value is displayed as empty ...

Finding a date from a calendar with a readonly property in Playwright

Just starting out with the playwright framework after working with Protractor before. I'm trying to figure out the correct method for selecting a date in Playwright. selector.selectDate(date) //having trouble with this ...

Enhance Angular Forms: Style Readonly Fields in Grey and Validate Data Retrieval using Typescript

Is there a way to disable a form and make it greyed out, while still being able to access the values? 1/ Disabling controls with this.roleForm.controls['id'].disable() in Typescript does not allow me to retrieve the values of Id, Name, and Descr ...

Determine if an element in Angular 6 contains a particular style

Below is a div, and the first time you click on it, an opacity style is added. I am looking to determine within the same function if this div has an opacity style set to 1. @ViewChild('address') private address: ElementRef; public onClickAddres ...

"Update your Chart.js to version 3.7.1 to eliminate the vertical scale displaying values on the left

Is there a way to disable the scale with additional marks from 0 to 45000 as shown in the screenshot? I've attempted various solutions, including updating chartjs to the latest version, but I'm specifically interested in addressing this issue in ...

Notify user before exiting the page if there is an unsaved form using TypeScript

I am working on a script that handles unsaved text inputs. Here is the code for the script: export class Unsave { public static unsave_check(): void { let unsaved = false; $(":input").change(function(){ unsaved = true; ...

Resolving the issue of missing properties from type in a generic object - A step-by-step guide

Imagine a scenario where there is a library that exposes a `run` function as shown below: runner.ts export type Parameters = { [key: string]: string }; type runner = (args: Parameters) => void; export default function run(fn: runner, params: Parameter ...

Exploring Angular: The Dynamic Declaration of object.property in ngModel

<input [(ngModel)]="Emp."+"dt.Rows[0]["columnname"]"> This scenario results in an undefined value In my current project, I am leveraging the power of a MVC CustomHtmlHelper to generate textboxes dynamically based on database schema. The textbox ...

The AngularJS 2 TypeScript application has been permanently relocated

https://i.stack.imgur.com/I3RVr.png Every time I attempt to launch my application, it throws multiple errors: The first error message reads as follows: SyntaxError: class is a reserved identifier in the class thumbnail Here's the snippet of code ...

The current Angular 11 build seems to lack in producing sufficient Lazy chunk files

Currently, I am working on implementing lazy loading for modules from different libraries in my project. This involves utilizing two libraries located in the node_modules directory, which are then lazily loaded by the main application. Below is a snippet o ...

When an empty array is returned from a catch statement in an async/await method, TypeScript infers it as never

Function getData() returns a Promise<Output[]>. When used without a catch statement, the inferred data type is Output[]. However, adding a catch statement in front of the getData() method changes the inferred data type to Output[] | void. This sugge ...

Creating HTML elements dynamically based on the value of a prop within a React component

In my React component built using Typescript, it takes in three props: type, className, and children The main purpose of this component is to return an HTML element based on the value passed through type. Below is the code for the component: import React ...

Can someone please explain how to prevent Prettier from automatically inserting a new line at the end of my JavaScript file in VS Code?

After installing Prettier and configuring it to format on save, I encountered an issue while running Firebase deploy: 172:6 error Newline not allowed at end of file eol-last I noticed that Prettier is adding a new line at the end when formatting ...

Swap out the default URL in components with the global constant

Could anyone offer some assistance with this task I have at hand: Let's imagine we have a global constant 'env' that I need to use to replace template URLs in components during build time. Each component has a default template URL, but for ...

Ensure that the form is submitted only after confirming it in the modal with react-hook-form

**I am facing an issue with submitting react-hook-form on confirm in a modal component. Whenever the user selects confirm, I need the form to be submitted directly. I have tried writing the modal inside FormSubmitButton. Also, I have tried placing it insi ...