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

Angular 9: The instantiation of cyclic dependencies is not allowed

After transitioning from Angular 8 to Angular 9, I encountered an issue with a previously functioning HTTP communication service. The error message now reads: Error: Cannot instantiate cyclic dependency! HttpService at throwCyclicDependencyError (core ...

Typescript loading icon directive

Seeking to create an AngularJS directive in TypeScript that wraps each $http get request with a boolean parameter "isShow" to monitor the request status and dynamically show/hide the HTML element depending on it (without utilizing $scope or $watch). Any ...

Error code -8 occurred while executing the yarn dev command, unable to identify the issue

I'm facing an issue with my development script that is structured like this: "scripts": { "dev": "./test.sh", }, Upon running the 'yarn dev' command, I encounter the following error message: Internal Error: ...

The issue of returning a boolean value in an rxjs function leading to failure

Hey there, I am currently learning about rxjs and I want to create an observable that returns either true or false. This is my attempted code: checkLoggedIn(): Observable<boolean> { // Check with the server if the user is logged in if(this._tok ...

Managing Modules at Runtime in Electron and Typescript: Best Practices to Ensure Smooth Operation

Creating an Electron application using Typescript has led to a specific project structure upon compilation: dist html index.html scripts ApplicationView.js ApplicationViewModel.js The index.html file includes the following script tag: <script ...

Just completed the upgrade of my Angular project from version 9 to version 12, but now encountering issues with a module that utilizes Plotly

Here is the content of my app module file. All components and imports are in their respective places as specified in the documentation: import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from &apos ...

Enhanced string key indexer type safety in TypeScript

Discover and explore this online TypeScript playground where code magic happens: export enum KeyCode { Alt = 'meta', Command = 'command', // etc. } export type KeyStroke = KeyCode | string; export interface Combination { comb ...

Error: Failed to locate package "package-name" in the "npm" registry during yarn installation

While working on a large project with numerous sub-projects, I attempted to install two new packages. However, YARN was unable to locate the packages despite the .npmrc being present in the main directory. ...

Using methods from one component in another with NgModules

There are two modules in my project, a root module and a shared module. Below is the code for the shared module: import { NgModule } from '@angular/core'; import { SomeComponent } from "./somecomponent"; @NgModule({ declarations: [SomeCompon ...

Why is webpack attempting to package up my testing files?

In my project, I have two main directories: "src" and "specs". The webpack configuration entrypoint is set to a file within the src directory. Additionally, the context of the webpack config is also set to the src directory. There is a postinstall hook in ...

When using TypeScript, my sorting function returns a value of 0 for all time values

I have been trying to sort this JSON data by date using the provided code, but it does not seem to work as expected. Below is a snippet of my JSON: { "StatusCode":0, "StatusMessage":"OK", "StatusDescription":[ { "id":"1", ...

Access SCSS variable values in Angular HTML or TypeScript files

So, I've been looking into whether it's feasible to utilize the SCSS variable value within HTML or TS in Angular. For instance: Let's say I have a variable called $mdBreakpoint: 992px; stored inside the _variable.scss file. In my HTML cod ...

Command to update a document in AWS DynamoDB using the Document Client

While attempting to utilize the UpdateCommand feature within the AWS DynamoDB documentation, I encountered various challenges due to its lack of detailed explanation and difficulty in implementation. My aim was to employ the update command to seamlessly t ...

Setting a value to an optional property of an inherited type is a simple task that can

export interface CgiConfiguration { name: string, value?: string } export interface CgiConfigurationsMap { [configurationName: string]: CgiConfiguration } const createCGI = <T extends CgiConfigurationsMap>(configurations: T) => configur ...

When utilizing useRef and useCallback in React, the output is visible in the console log but does not appear on the page

When working with API data, it's important to remember that the extraction process is asynchronous and the state may not be available at certain times. To handle this situation, we can utilize useCallback. However, even after successfully logging the ...

Function overloading proving to be ineffective in dealing with this issue

Consider the code snippet below: interface ToArraySignature { (nodeList: NodeList): Array<Node> (collection: HTMLCollection): Array<Element> } const toArray: ToArraySignature = <ToArraySignature>(arrayLike: any) => { return []. ...

What steps can be taken to resolve the error message "Module '../home/featuredRooms' cannot be found, or its corresponding type declarations"?

Upon deploying my site to Netlify or Vercel, I encountered a strange error. The project runs smoothly on my computer but seems to have issues when deployed. I am using TypeScript with Next.js and even attempted renaming folders to lowercase. Feel free to ...

Handling HTTP errors in Angular when receiving a JSON response

I'm struggling to resolve this issue and I've searched online with no luck. The problem lies in my post call implementation, which looks like this: return this.http.post(url, body, { headers: ConnectFunctions.getHeader() }).pipe( map(result =&g ...

Adjust the size of an Angular component or directive based on the variable being passed in

I'm looking to customize the size of my spinner when loading data. Is it possible to have predefined sizes for the spinner? For example: <spinner small> would create a 50px x 50px spinner <spinner large> would create a 300px x 300p ...

What is the most graceful method to define a class attribute just once?

Is there a clean and efficient way to set a value only once within a TypeScript class? In other words, is there a way to make a value read-only after it has been assigned? For instance: class FooExample { public fixedValue: string; public setFixe ...