Wrapper around union function in TypeScript with generics

I'm struggling to find a solution for typing a wrapper function. My goal is to enhance a form control's onChange callback by adding a console.log. Can someone please point out what I might be overlooking?

interface TextInput {
    type: 'TextInput';
    onChange: (value: string) => void;
}

interface NumberInput {
    type: 'NumberInput';
    onChange: (value: number) => void;
}

type FormControl = TextInput | NumberInput;

export const withLogger = <TFormControl extends FormControl>(formControl: TFormControl): TFormControl => ({
    ...formControl,
    onChange: (...args: Parameters<TFormControl['onChange']>) => {
        console.log(formControl.type, 'onChange', args);
        // Issue: A spread argument must either have a tuple type or be passed to a rest parameter.(2556)
        return formControl.onChange(...args);
    },
});

I've tried various ways of defining the arguments for the onChange wrapper such as:

onChange: (value: any) => formControl.onChange(value)
onChange: <TValue>(value: TValue) => formControl.onChange(value)
onChange: (value: number | string) => formControl.onChange(value)

However, all these attempts result in an error message:

Argument of type 'string | number' is not assignable to parameter of type 'never'. Type 'string' is not assignable to type 'never'.(2345)

Edit: TypeScript playground link

Answer №1

When it comes to conditional types that rely on generic type parameters, the TypeScript compiler faces limitations in terms of analysis. Due to the nature of conditional types and how they interact with generic type parameters like Parameters<T>, which are implemented as conditional types themselves, the compiler struggles to determine the specifics of

Parameters<TFormControl['onChange']>
. This uncertainty poses challenges when verifying its compatibility with functions of type TFormControl['onChange'].

The inability of the compiler to fully grasp the inner workings of Parameters<T> leads to scenarios where formControl.onChange(...args) is misconstrued, causing a widening effect from the original generic TFormControl to the non-generic FormControl union type. Consequently, calling a union of functions becomes problematic.


A suggested remedy involves refactoring your code so that generic operations are handled through indexed accesses into homomorphic mapped types rather than relying on conditional types. Detailed guidance on this technique can be found in microsoft/TypeScript#47109.

You can begin by defining a simple mapping object, connecting literal types like "TextInput" and "NumberInput" to their respective parameter list types:

interface InputArgs {
    TextInput: [value: string];
    NumberInput: [value: number];
}

Subsequently, introduce Input<K> as a distributive object type over K, allowing for indexing into a mapped type over K to yield a union:

type Input<K extends keyof InputArgs = keyof InputArgs> =
    { [P in K]: { type: P, onChange: (...args: InputArgs[P]) => void } }[K];

This consolidated Input type encompasses the functionalities of TextInput, NumberInput, and FormControl:

type TextInput = Input<"TextInput">;
type NumberInput = Input<"NumberInput">;
type FormControl = Input;

With this setup, you can implement withLogger() as follows:

export const withLogger = <K extends keyof InputArgs>(formControl: Input<K>): Input<K> => ({
    ...formControl,
    onChange: (...args: InputArgs[K]) => {
        console.log(formControl.type, 'onChange', args);
        return formControl.onChange(...args); // okay
    },
});

This method ensures that onChange for an Input<K> maintains its generic qualities, accepting arguments of type InputArgs[K]. As a result, no unnecessary widening occurs, preserving the usability of the function and its arguments.

Playground link to code

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

Tips for arranging mat-option in alphabetical order using typescript in Angular 7 Material Design

According to the Angular documentation, there is currently no orderBy pipe available for sorting. It is recommended to implement the sort functionality in the component. However, as a beginner in Angular, I am unsure of how to do this. Can anyone provide a ...

Incorporate the Input() component into your codebase and take advantage of its dot notation features, such as

Many Angular directives utilize dot notation options: style.padding.px style.padding.% attr.src In addition, libraries like flex-layout employ this for various responsive sizes: fxLayout.gt-sm fxAlign.sm Can the same concept be applied to a component&a ...

Ways to turn off Typescript alerts for return statements

I'm looking to turn off this Typescript warning, as I'm developing scripts that might include return values outside of a function body: https://i.stack.imgur.com/beEyl.png For a better example, check out my github gist The compiled script will ...

Angular service is able to return an Observable after using the .then method

I am currently facing an issue with retrieving the authentication status in a service method. Everything seems to be working fine except for the return statement. I am struggling with the usage of .then inside .map and I am unable to figure out how to retu ...

Error occurred after attempting to make a GET request

What I aim to achieve: I need to send two parameters from the front-end to the back-end. This is the code snippet: In TypeScript file: activeFromActiveToQuery(req?: any): Observable<ResponseWrapper>{ const options = createRequestOption(req) ...

Is there a way to utilize a value from one column within a Datatables constructor for another column's operation?

In my Typescript constructor, I am working on constructing a datatable with properties like 'orderable', 'data' and 'name'. One thing I'm trying to figure out is how to control the visibility of one column based on the va ...

Sequelize's bulk synchronization process is ineffective

I am facing an issue with getting sequelize.sync() to function properly. When I call sync() for each model definition individually, it works perfectly fine. However, when trying to execute it from the sequelize instance itself, it seems like the registered ...

Issue with routing in a bundled Angular 2 project using webpack

Having a simple Angular application with two components (AppComponent and tester) webpacked into a single app.bundle.js file, I encountered an issue with routing after bundling. Despite trying various online solutions, the routing feature still does not wo ...

What is the reason for a boolean extracted from a union type showing that it is not equivalent to true?

I'm facing a general understanding issue with this problem. While it seems to stem from material-ui, I suspect it's actually more of a typescript issue in general. Despite my attempts, I couldn't replicate the problem with my own types, so I ...

Tips for determining if a key is present in local storage:

I need to set a key value, but only if it doesn't already exist. In my component1.ts file, I am assigning the key and value in the constructor. However, I want to include a condition that this action should only be taken if the key is not already pre ...

Converting a string into a Date in Typescript while disregarding the timezone

Upon receiving a date in string format like this (e.g.): "11/10/2015 10:00:00" It's important to note that this is in UTC time. However, when creating a Date object from this string, it defaults to local time: let time = "11/10/2015 10:00:00"; let ...

What is the best method for retrieving GET parameters in an Angular2 application?

Is there a way in Angular2 to retrieve GET parameters and store them locally similar to how sessions are handled in PHP? GET Params URL I need to obtain the access_token before navigating to the Dashboard component, which makes secure REST Webservice cal ...

Issue with Angular 10 Web Worker: Unable to locate the main TypeScript configuration file 'tsconfig.base.json'

Every time I attempt to run: ng g web-worker canvas I consistently encounter the error message: Cannot find base TypeScript configuration file 'tsconfig.base.json'. After thorough examination of my files, it appears that I am indeed missing a ...

Tips for accessing the StaticRouterContext in Typescript with react-router-dom

Currently, I am implementing SSR for my app specifically targeting robots. There is a possibility that the render of the <App/> component may lead to a route not being found. In order to handle this scenario, I need to identify when the render ends ...

What is the best way to set up the typeRoots option for proper configuration

I have a unique yarn monorepo structure that is oddly shaped. Here's how it's set up: monorepo root ├── frontend │ ├── dashboard <-- not managed by yarn workspaces │ | ├── src │ | ├── node_modules │ ...

Watchable: Yield the outcome of a Promise as long as watching continues

I am looking to create a function in Angular and TypeScript that will return an Observable for subscription. This Observable should emit the result of a Promise every three seconds. Currently, I have a function that returns a Promise, but I need it to ret ...

Does a typescript definition file exist for Apple MapKit JS?

Before embarking on creating one, I'm curious if anyone has come across a typescript definition file (.d.ts) for Apple MapKit JS? ...

The operation failed with a TypeError because the object does not allow the addition of the newField property

I encountered an error that says: TypeError: Cannot add property newField, object is not extensible Whenever I try to add a new key or change a value, it doesn't work and I'm not sure what the issue is. dynamicFilter; public ionViewWillEnte ...

Set the timezone of a Javascript Date to be zero

Is there a way to create a Javascript date without any specific timezone? When I try to do so in Javascript, it automatically sets it to GMT Pacific standard time. let newDate = new Date(new Date().getFullYear(), 0, 2, 0, 0, 0, 0) }, newDate: Sat Feb 01 2 ...

What is the best way to add all the items from an array to a div element?

I am currently facing an issue where only the last object in my array is being added to my div using the code below. How can I modify it to add all objects from the array to my div? ajaxHelper.processRequest((response: Array<Vehicle.Vehicle>) ...