I'm having trouble grasping the concept of using generics with promises in TypeScript

In this code snippet, we have a function that accepts a promise and a timeout value as arguments, then returns another promise. If the initial promise does not resolve within the specified timeout period, the new promise will reject immediately. On the other hand, if the initial promise resolves before the timeout, the new promise will also resolve with the same value.

function resolveOrTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    // set up timeout functionality
    const task = setTimeout(() => reject("time up!"), timeout);

    promise.then(val => {
      // cancel the timeout
      clearTimeout(task);

      // resolve with the value from the original promise
      resolve(val);
    });
  });
}
resolveOrTimeout(fetch(""), 3000);

The concept behind this function is clear, however, I am puzzled by the type annotations used here, particularly the implementation of generics. Generics in TypeScript are used to parameterize types, similar to how functions parameterize values. But in this case, why do we need to use generics for types? Moreover, even when the type variables like T are not explicitly provided during the function call, the compiler does not flag any errors. This behavior seems perplexing to me.

Answer №1

Just like an empty Array, a Promise on its own is not particularly captivating. It simply serves as a placeholder for a future value, a container waiting to be filled with a specific type of content. Rather than creating multiple functions tailored to handle different types of values that could go inside the box, we can tell the compiler to either let the caller specify the type or attempt to deduce it based on usage.

For example, consider a function that retrieves the last element of an array, initially in plain JavaScript:

function last(arr) {
  return arr[arr.length - 1];
}

Now, let's incorporate type annotations:

function last<T>(arr: T[]): T {
  return arr[arr.length - 1];
}

The goal here is to make the function versatile and applicable to arrays containing various data types:

const x = last(['a', 'b']); // compiler can infer x is a string
const y = last([1, 2]);     // compiler can infer y is a number
const z = last<number>(['a', 3]); // explicitly specifying a heterogenous array

No one wants to manually define overloaded functions for every possible array variation, which is where generics come in handy.

Answer №2

Initially, the generic type parameter passed to Promise determines the type it will yield upon resolution.

For instance:

function numberInThreeSeconds(): Promise<number> {
    return new Promise(resolve => {
        setTimeout(() => resolve(123), 3000)
        //                       ^ generic paramter type passed to Promise
    })
}

Therefore,...

"I comprehend that generics parameterize types similar to how functions parameterize values, but why is it necessary to parameterize types in the first place?"

This allows the resolved value of resolveOrTimeout to have a defined type.

resolveOrTimeout(numberInThreeSeconds(), 5000)
  .then(val => console.log(val * 2)) // val is known to be of type number

Since numberInThreeSeconds resolves to a number, using generics enables the function to return a promise that resolves to the same type as its initial argument.


"Even if the function call doesn't specify the type variables as T in the generics, why doesn't the compiler flag any errors?"

Generic functions can infer their parameters based on usage. For example:

function identity<T>(arg: T): T { return arg }
const a = identity('abc') // string
const b = identity(123) // number

By knowing the argument's type, TypeScript infers T as that same type and returns it as well.

The same principle applies to resolveOrTimeout<T>.

function resolveOrTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {}

The argument promise: Promise<T> allows TypeScript to deduce T by ensuring the input is a promise and inspecting its resolved type.

Consequently, the function can also return Promise<T>, indicating the output promise resolves to the same type as the promise parameter.


Further insights on how generics and inference operate can be found here

Answer №3

When it comes to the use of generics, the generic T may not always seem useful at first glance. However, if we were to modify the code like so:

function resolveOrTimeout<T>(promise: Promise<T>, timeout: T): Promise<T> {
}

By providing an input, the compiler is able to infer that the function will return a promise with the same type as the parameter timeout. This allows for a more typesafe approach when writing code that can still be considered "generic."

Generics are typically used in cases where the specific type of a value is not crucial to the context. For instance, consider a scenario where a function simply places the generic parameter into an array. In this case, the exact type of the parameter isn't necessary since it's only being stored in an array. By using generics, TypeScript can recognize that the function accepts any type of parameter and returns an array of the same type.

function toArray(input: T): T[] {
    return [input];
}

During compilation, the compiler will determine this behavior. If we were to try the following code, an error would occur indicating that toArray outputs a string[], but the variable is anticipating a number[]:

let someArray: number[] = toArray("Some string")

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

What is the most effective method for enlarging elements using Javascript?

I have come across many different methods for expanding elements on webpages, such as using divs with javascript and jquery. I am curious to know what the most optimal and user-friendly approach is in terms of performance, among other things. For instance ...

JavaScript plugin designed for effortless conversion of JSON data to structured HTML code

I want to add this JSON data to an HTML element. [ { "website":"google", "link":"http://google.com" }, { "website":"facebook", "link":"http://fb.com" } ] Is there an easy way to convert this using a plugin ...

Is your AJAX request exceeding its limits?

I'm currently working on a form that contains numerous fields. My plan is to submit all the data asynchronously via AJAX. However, upon clicking the Submit button, I noticed that it remains in its clicked state and the page freezes. Additionally, the ...

Organizing communications in NodeJS messaging application

My latest project involves creating a chat room using NodeJS and socket.io which allows multiple users to connect with unique usernames. Everything is running smoothly, except for one minor issue. I want the messages sent by me to appear in a different co ...

Storing mouse coordinates during mousemove in AngularJS

As I attempt to adapt a functional code that previously worked without Angular to now function with Angular, I'm starting to realize that my understanding of Angular may not be as clear as I originally thought: The existing setup consists of multiple ...

The Web API controller is able to successfully download a CSV file, however, it is facing compatibility issues

I have set up a basic web api controller that can be accessed through a URL to download a .csv file. Now, I am attempting to implement some jQuery so that when a button is clicked, the file will be downloaded. However, I seem to be missing a crucial elemen ...

What is the process for manually triggering an observable error?

I'm currently developing an Angular application where I am making a REST call using HTTP. Here is the code snippet: login(email, password) { let headers = new Headers(); headers.append('Content-Type', 'application/x-www-form-url ...

Locate the ID of the element targeted by the cursor in a contenteditable field

Within a contenteditable div, I have various child elements with different ids. When the cursor is moved inside the contenteditable tag, I want to retrieve the id of the element at the cursor position. For example: If the cursor is on the word "one," th ...

No selection made in Angular2 dropdown menu

I can't seem to figure out what I am doing wrong. I am trying to get the value of a dropdown to be selected in my form. Here is the template-driven form I am using: <select class="form-control" id="role" re ...

Aggregate data from NodeJS employees

I'm dealing with a scenario where I have master and workers running parallel computations. How can I aggregate the results produced by each worker? Once each worker completes its task, it terminates itself and any result stored in the 'res&apos ...

Discovering the wonders of Angular's Heroes Tour: What's the magic behind adding the ID in the addHero function

During Angular's official tour of heroes tutorial (https://angular.io/tutorial/toh-pt6), an interesting observation was made. When the addHero method is called, only the hero's name property is passed as an argument. However, it seems that a new ...

Is it possible to combine multiple jQuery selectors for one command?

I need some help with jQuery commands. I have two similar commands, but not exactly the same. Here are the commands: $("#abc > .className1 > .className2 > .className3"); $("#pqr > .className1 > .className2 > .className3"); Is there a w ...

Leveraging vanilla web components in combination with a bootstrap template

Currently, my ASP.NET MVC website is styled using the Metronic Bootstrap template. I am contemplating the idea of revamping this website by implementing the vanilla web components approach. In a preliminary test, I created a web component without shadow ...

XMLHttpRequest Error: The elusive 404 code appears despite the existence of the file

This is the organization of my project files: The folders Voice, Text, and Template are included. https://i.stack.imgur.com/9un9X.png When I execute python app.py and navigate to localhost http://0.0.0.0:8080/, the index.html page is displayed with conte ...

Disabling certain dates with jQuery datepicker does not provide flawless results

I am having trouble disabling certain dates in the datepicker calendar. Even after including all the necessary jQuery files, the dates are still not being disabled as expected. Required scripts: <script src="https://ajax.googleapis.com/ajax/libs/jquer ...

Getting the parameter route value from Laravel and passing it to Ajax

Need help with returning the parameter of a Laravel route to an AJAX response. Here is the relevant code: public function getPermissions(Request $request) { //$v = request()->route()->parameters('profile'); $v = request()-& ...

What is the process for establishing a connection between a websocket and an external API?

Currently, I have a route configured to fetch the weather data for a specific city using the openweathermap API Here is the code snippet from my index.js file: var express = require("express"), router = express.Router(); var weather = require("ope ...

Issue with importing client file in discord.js causing errors

Currently, I am attempting to bring in sections of the bot.js file and refer to it as Client const Topgg = require('@top-gg/sdk') const { message } = require('./bot') const Client = require('./bot') const Discord = require(&ap ...

Launching a program through a web browser - a step-by-step guide

I am looking to create a specific sequence of events: A web browser opens, and a user logs in (the exact action is not crucial) The user is then redirected to a new page where another program should automatically open This process is similar to what happ ...

Is it possible for entities to transition between different use cases by means of the controller?

Currently, I am tasked with developing a route for processing ticket purchases for events. My approach involves utilizing a controller to handle incoming requests containing order data, client details, ticket information, and payment specifics. These reque ...