What is the best way to use a generic callback function as a specific argument?

TS Playground of the problem

function callStringFunction(callback: (s: string) => void) {
  callback("unknown string inputted by user");
}

function callNumberFunction(callback: (n: number) => void) {
  callback(4); // unknown number inputted by user
}

function genericFunction<T extends string | number>(value: T, genericCallback: (newValue: T) => void) {
  if (typeof value === "string") {
    console.log(value); // T is known to be a string here
    callStringFunction(genericCallback); // But this throws an error: (newValue: T) => void not assignable to (s: string) => void
  }
  else {
    console.log(value); // T is known to be a number here
    callNumberFunction(genericCallback); // But this throws an error: (newValue: T) => void not assignable to (n: number) => void
  }
}

How can I achieve this without using:

callNumberFunction(cb as (n: number) => void)

Given that the type for value has been verified, shouldn't TypeScript also be aware of the callback type?

Answer №1

Regrettably, the TypeScript compiler struggles to comprehend the logic in your approach here. When you use

typeof value === "string"
, the compiler views it as a type guard that narrows down value from T to either string or number. However, it does not narrow the type parameter T itself.

You were hoping for the compiler to interpret

typeof value === "string"
and value: T together and modify T from T extends string | number to T extends string or T extends number, or even just specific types like string and number. Unfortunately, this does not occur. T remains as T throughout the function, making
callStringFunction(genericCallback)
unacceptable. The compiler still perceives that T might be any arbitrary subtype of string | number.

There are ongoing feature requests on GitHub to address this situation in a better way; refer to microsoft/TypeScript#33014 for more details. However, no solution has been implemented yet, partly because certain "obvious" implementations could lead to unsoundness.

For instance, if I had

function f<T extends string | number>(value1: T, value2: T): void;
, I would not be able to assert that if value1 is a string, then value2 must also be a string. It is still possible for both value1 and value2 to be string | number, as shown in the function call
f(Math.random()<0.99 ? "" : 123, 123);
. There is a 99% chance of value1 being a string, while value2 is a
number</code. Any solution to address this would need careful consideration.</p>
<hr />
<p>Furthermore, I cannot find an effective way to restructure your code so that the compiler can guarantee its safety. The types <code>string
and number are not recognized as discriminant property types, preventing me from transforming the argument list to genericFunction() into a discriminated union. This approach only works if you pass in a literal parameter like
function genericFunction<K extends "str" | "num">(type: K, val: Val[K], cb: (x: Val[K])=>void): void;
. In that case, you could verify type, and then the compiler would narrow down both val and cb. The current structure of your code does not align with this, making it challenging to refactor directly.

As a temporary solution, I recommend using a type assertion to inform the compiler that you are aware of the type correctness. You can utilize val as Type where you are confident that val is indeed of type

Type</code, as demonstrated below:</p>
<pre><code>function genericFunction<T extends string | number>(
  value: T, genericCallback: (newValue: T) => void
) {
  if (typeof value === "string") {
    console.log(value); // T is now identified as a string at this point
    callStringFunction(genericCallback as (newValue: string) => void);
  }
  else {
    console.log(value); // T is now identified as a number at this point
    callNumberFunction(genericCallback as (newValue: number) => void);
  }
}

This approach resolves the errors by shifting the responsibility of ensuring type safety onto you. While not ideal, it seems to be the best workaround for the time being, without transitioning to a visibly distinct algorithm. It's a compromise for now!

Click here to view code in 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

Are there any risks associated with using nested setTimeout functions with the same name?

While reviewing the source code of typed.js, I noticed that the main function in this plugin utilizes a design pattern with multiple setTimeout functions nested inside one another. Here is a snippet of the code: self.timeout = setTimeout(function() { / ...

Do I have to divide the small functions in my Node.js controller into smaller ones?

When signing up users in my controller, do I need to break up the sign-up steps into individual asynchronous calls or is one big asynchronous call sufficient? Each step relies on the previous one: Validate user Create user Create group Add user to group ...

The color of the background does not remain changed permanently

I'm having an issue where the background color changes when I click a button, but reverts back almost immediately to its original color. How can I ensure that the new color stays permanently? Here is a simple JavaScript function you can use: functio ...

Exported data in Vue3 components cannot be accessed inside the component itself

Essentially, I'm working on creating a dynamic array in Vue3. Each time a button is clicked, the length of the array should increase. Below is the code snippet. <div class="package-item" v-for="n in arraySize"></div> e ...

What causes the 'cannot read properties of null' error when using dot notation in React, and what are the possible solutions to resolve it?

When I employ the conventional method of accessing objects using dot notation {quote.text} {quote.author}, I encounter a "cannot read properties of null" error message. import { useState, useEffect } from 'react'; import "./React.css"; ...

Add data to a size guide

My coding project involved creating a sizing chart using HTML, CSS, and JavaScript. The chart allows users to select their preferred units of measurement (metric or imperial). I used JavaScript to dynamically update the values in the chart based on the sel ...

The Slide Out Menu functioned properly in HTML, but it seems to be malfunctioning when implemented in PHP

I recently implemented a slide-in and out menu with a hamburger icon to open it and an X icon to close it. The functionality was perfect until I integrated it into PHP and WordPress. Initially, I placed the script in the header section before the meta tags ...

Reset the input field once the message has been successfully sent

My goal is to clear the form message input field after the form is sent. However, it seems to be clearing the data before it has a chance to be sent. Here is the code I'm using: <script> $(function () { $('form#SendForm'). ...

What is the best way to ensure a specific row remains at the bottom of an Angular table with Material when sorting?

My data table contains a list of items with a row at the end showing the totals. | name | value1 | value2 | --------------------------- | Emily | 3 | 5 | | Finn | 4 | 6 | | Grace | 1 | 3 | | TOTAL | 8 | 14 | I&apos ...

What is preventing jQuery 3 from recognizing the '#' symbol in an attribute selector?

After updating my application to jQuery 3, I encountered an issue during testing. Everything seemed to be working fine until I reached a section of my code that used a '#' symbol in a selector. The jQuery snippet causing the problem looks like th ...

Update not reflected in parent form when using ValueChanges in Angular's ControlValueAccessor

Here is the code snippet I am currently working with: https://stackblitz.com/edit/angular-control-value-accessor-form-submitted-val-egkreh?file=src/app/app.component.html I have set default values for the form fields, but when clicking the button, the pa ...

"Encountering a problem with ThreeJs graphics rendering

I recently developed an application using ThreeJs, but I encountered a strange issue. After rendering the 3D graphics, the window appears blank. However, when I click on full screen or adjust the window size, the output becomes visible. Check out Screen s ...

The Jquery script is ineffective for newly added elements

After creating an Ajax Form that functions well, I noticed that when the result of the form is another form, my script does not work for the new form generated. Is there a way to make the new form function like the old one? $(document).ready(function() ...

Is there a way to trigger a Javascript alert and then navigate to a different page in Django once the user presses 'ok'?

On my Django website, I have a contact form that triggers when the submit button is clicked. I want to display a Javascript alert notifying the user that their message has been sent and then redirect them back to the homepage. While I have successfully i ...

Utilizing jquery to showcase the information in a neat and organized table

Having an input text box and a button, I am looking to display dummy data in a table when any number is entered into the input field and the button is clicked. Here is what I have tried: My Approach $("button#submitid").click(function () { $(&quo ...

Adjusting the StrokeWidth in pixels/em units on fabricjs

When adding a rectangle or circle to the canvas and adjusting the strokeWidth using a range input, how can we determine the value of strokeWidth in pixels, em, or percent? For instance, if we set strokeWidth to 5 within a range of 1-10, how much does the a ...

"Encountering difficulties while setting up an Angular project

I am currently working on setting up an Angular project from scratch. Here are the steps I have taken so far: First, I installed Node.js Then, I proceeded to install Angular CLI using the command: npm install -g @angular/cli@latest The versions of the ...

Accessing and manipulating a scope variable in Angular from an external source

Can you update a $scope variable in a controller from an external source? For instance, suppose we have the following controller: app.controller('citySelectCtrl', ['$scope',function($scope){ }]); And there is a function in the globa ...

Is it possible to monitor and keep a record of every modification made to an HTML element?

Can anyone suggest an effective method to monitor changes made to an HTML element? I attempted to utilize JavaScript with jQuery but was unsuccessful. $('div.formSubmitButton input[type="submit"]').change(function(event){ alert( ...

Nextjs Clerk: The <SignIn/> element lacks proper configuration

I attempted to resolve the issues indicated in the error message below, but unfortunately, I was unsuccessful. How can I troubleshoot this error and perform an “auth-callback with clerk”? - The technologies used include Nextjs, Nextjs Actions, Clerk, a ...