Generic function's contravariance predicament

Suppose you have the following type:

type TComp <T> = (cb: (arg: T) => void, value: T) => void;

and two different implementations of this type:

const f1: TComp<number> = (cb: (a: number) => void, value: number) => {
    cb(value + 122);
}
const f2: TComp<string> = (cb: (a: string) => void, value: string) => {
    cb(value + "ok");
}

Now, let's introduce string identifiers for types:

type TStrNum = 'str'|'num';

type TNameTypeMap = {
    'str': string;
    'num': number;
}

We want to create a function TComp <T> with the following conditions:

const sf = <T extends TStrNum>(cb: (a: TNameTypeMap[T]) => void, value: TNameTypeMap[T], name: TStrNum) => {
    if(name==='num') {
        return f1(cb, value); //1 ts complains on `cb`
    }
    if(name==='str') {
        return f2(cb, value); //2 ts complains on `cb`
    }
}

When TypeScript encounters //1, it throws the error:

Argument of type '(a: TNameTypeMap[T]) => void' is not assignable to parameter of type '(arg: number) => void'.   
 Types of parameters 'a' and 'arg' are incompatible.     
  Type 'number' is not assignable to type 'TNameTypeMap[T]'.       
   Type 'number' is not assignable to type 'never'.

Similarly, in //2, the same issue arises but with string.

This problem seems to be associated with contravariance of cb, but I am unsure of the exact cause.

Is there a solution to this problem, or perhaps another way to implement this function?

Answer №1

Prior to delving into the implementation details, it's essential to address a key issue from the perspective of the function caller. By defining the name parameter as TStrNum, we fail to enforce a requirement that the name parameter must align with the value and cb parameters. Consequently, the following call can be made without triggering any compiler warnings:

sfOrig<"num">((a: number) => a.toFixed(), 123, "str"); // No compiler error,
// RUNTIME 💥 TypeError: a.toFixed is not a function 

This presents an issue. To resolve this, simply change the type of name to T:

const sf = <T extends TStrNum>(
  cb: (a: TNameTypeMap[T]) => void, 
  value: TNameTypeMap[T], 
  name: T
) => {
  if (name === 'num') {
    return f1(cb, value); // Compiler error
  } else {
    return f2(cb, value); // Compiler error
  }
}

With this adjustment, mismatched inputs will now trigger a compiler error:

sf((a: number) => a.toFixed(), 123, "str"); // Compiler error!
// ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Argument of type '(a: number) => string' is not assignable 
// to parameter of type '(a: string) => void'

sf((a: number) => a.toFixed(), 123, "num"); // Okay

Once the call signature is in place, attention can be directed towards the implementation. TypeScript's type analysis has limitations when it comes to tracking correlations between expressions of union types or constrained generic types. It currently lacks a mechanism to constrain a type parameter like T to one of the members of a union such as TStrNum.

If maintaining type safety while retaining ease of use for the caller is desired, consider utilizing a discriminated union as an input data structure instead of multiple correlated parameters:

type Args = { [P in TStrNum]:
  [cb: (a: TNameTypeMap[P]) => void, value: TNameTypeMap[P], name: P]
}[TStrNum]

const sf = (...args: Args) => { /* Implementation goes here */ }

This setup ensures that calls adhere to specific forms, either

sf((a: string)=>{}, "", "str")
or
sf((a: number)=>{}, 0, "num")
. Mixing different forms is prevented, and the discriminant element in
Args</code makes it a discriminated union.</p>
<p>Regarding the implementation process, it's important to avoid immediate destructuring of the arguments tuple to maintain correlation between them. Instead, keeping the tuple intact helps retain discriminated union narrowing:</p>
<pre><code>const sf = (...args: Args) => {
  if (args[2] === 'num') {
    return f1(args[0], args[1]);
  } else {
    return f2(args[0], args[1]);
  }
}

By following this approach, errors are minimized both at the calling side and within the implementation.

Explore the code in TypeScript Playground

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

Struggling with object type casting in Typescript

Having issues with casting objects from an HTTP API response to Typescript. I am trying to cast the json data to a Typescript object using the "as" keyword or <Type >, but it's not working as expected. r.forEach(entry => { entry.creatio ...

Typescript is having issues with the Foreach function

Can someone explain how I can utilize foreach in this scenario to showcase all the names? exampleData: [{ id: "1", name: "John" }, { id: "2", name: "Jane" }] The following code snippet does not seem to be working: this.exampleData.forEac ...

How can I properly include DefinitelyTyped TypeScript definition files in a .NET Core project?

Previously, under asp.net for .net framework, I utilized NuGet to incorporate TypeScript type definitions from third-party libraries (*.d.ts files) provided by DefinitelyTyped. However, with the shift to .NET Core, it seems that NuGet is no longer recommen ...

Increasing numbers using Vuex Module Decorators and TypeScript

I'm encountering a major challenge with incrementing data. @Module class Mod extends VuexModule { public value = 2; // facing multiple type errors when trying to access this.state.value within a MutationAction. @MutationAction({ mutate: [" ...

Obtain the firebase object using Angular framework

Hey there, I've been working on retrieving a Firebase object using Angular and have successfully achieved that. However, I'm now faced with the challenge of how to navigate deeper into the data that is being returned (check out the images linked ...

Issue with detecting errors in Angular unit test when using jest throwError method

Imagine I have a component that contains the following method: someMethod() { this.someService .doServicesMethod(this.id) .pipe( finalize(() => (this.loading = false)), catchError((e) => { this.showErrorMessage = true; ...

A function in Typescript is created to handle diverse input types in a generic manner

My goal is to create a function that can handle various input types for abstraction purposes. type ContentA = string type ContentB = number type InputA = { name: 'method_a' content: ContentA } type InputB = { name: 'method_b' con ...

What methods can be implemented to ensure ComponentOverride's universality?

These type definitions for markdown-to-jsx don't seem to be generic enough, causing issues like the one mentioned below. For more details, refer to Why is type SFC<AnchorProps> not assignable to type SFC<{}>? /Users/sunknudsen/Sites/sunk ...

Rendering a component in React based on multiple conditions

Checking sessionStorage and another state variable before rendering a component is essential for my application. I want to avoid showing the same message multiple times within the same session. This is how I have implemented it: const addSession = (noteId: ...

Using V-For with data fetched from an axios request: A step-by-step guide

How can I dynamically populate V-Cards after making an Axios request to retrieve data? The Axios request is successful, but the v-for loop does not populate with V-Cards. I've also attempted to make the request before the rendering is completed (usin ...

How do I eliminate the request type following the request name in swagger-typescript-api?

I am in search of a method to derive types from a Swagger file without appending a specific request type to the request name. For instance: If I have an endpoint /assortment and the request type is POST, I currently receive the Api.assortmentCreate() meth ...

A function in Typescript designed to take in two objects that possess identical keys

I am looking to define a function that takes two parameters, each being an object. These objects have the same keys, but the data types of the values under those keys should be different (yet the same within each object). I attempted to achieve this using ...

Tips for configuring the global API baseUrl for useFetch in Nuxt 3

Is there a way to globally set the baseUrl used in the useFetch composable, possibly through nuxt.config.ts? How can I prevent having to specify it in each individual useFetch call? ...

Assign custom keys to request object parameters before reaching the controller in the map

I have a Loopback 4 application where the request object's property keys are in snake_case and they correspond to our database column names which are in StudlyCase. What I want is to change the application property names to camelCase. This means that ...

Develop instances from a class in Angular 2

Hello there! I have a question about creating a class that implements an interface and utilizes data from a .get service to instantiate a new object. Here is an example of what I'm trying to achieve: import { Component, OnInit } from '@angular/c ...

Data not being retrieved by HTTP GET request

I encountered an issue with my API where I made three Get requests using the same function but different URLs to differentiate between them. However, even though the provider returns the data in steps, the page response function does not receive it and sho ...

Acquiring the download link for Firebase Storage in Angular 2+ technology

reference: AngularFireStorageReference; task: AngularFireUploadTask; uploadState: Observable<string>; uploadProgress: Observable<number>; downloadLink: Observable<string>; beginUpload(event) { const id = Math.floor(Math.random() * 1000 ...

Utilizing custom property within an extended MatPaginatorIntl class

I have been experimenting with some code and you can find it here: Test my Code on Stackblitz To customize the labels of MatPaginator, I am using an extended MatPaginatorIntl class successfully. However, I want to introduce a custom variable (referred to ...

Effortless Tree Grid Demonstration for Hilla

As someone who is just starting out with TypeScript and has minimal experience with Hilla, I kindly ask for a simple example of a Tree Grid. Please bear with me as I navigate through this learning process. I had hoped to create something as straightforwar ...

Integrate a service component into another service component by utilizing module exports

After diving into the nestjs docs and exploring hierarchical injection, I found myself struggling to properly implement it within my project. Currently, I have two crucial modules at play. AuthModule is responsible for importing the UserModule, which conta ...