Joji
Joji

Reputation: 5615

TypeScript: cannot seem to understand this generics usage for promise

Here is a function, which takes a promise and a number which is for timeout and returns another promise. If the promise as the first argument doesn't resolve before the timeout, the promise returned from the function will reject immediately. Or it will resolve the value when the first promise resolves.

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

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

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

I understand the logic of the function and I am well aware of how promise works in JavaScript. But I just don't understand the type annotations here, specifically the usage of generics here. I understand that generics parameterize types like functions parameterize value, but here why do we need to parameterize the types in the first place? And also why is that even the function call doesn't provide the type variables as the T in the generics, the compiler doesn't report any errors?

Upvotes: 0

Views: 1060

Answers (3)

Alex Wayne
Alex Wayne

Reputation: 187034

First of all, generic type parameter you pass to Promise is the type that it will provide when it resolves.

A simple example:

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

So, given that...

"I understand that generics parameterize types like functions parameterize value, but here why do we need to parameterize the types in the first place?"

You do that so the resolved value of resolveOrTimeout has a known type.

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

Because numberInThreeSeconds is known to resolve to a number, then the usage of generics allows the function to return a promise that resolves to the same type as its first argument.


"And also why is that even the function call doesn't provide the type variables as the T in the generics, the compiler doesn't report any errors?"

Because generic functions can infer their generic parameters from usage. At it's simplest:

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

Because typescript knows the type of the argument, it can infer T as whatever that type is, and then it returns that same type T.

So the same thing is happening with resolveOrTimeout<T>.

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

The arg promise: Promise<T> tells typescript that it can infer T by requiring that the argument be a promise, and then looking inside that promise to find its resolved type.

And it can now return Promise<T> as well, which says that this function returns a promise that resolves to the same type as the promise argument.


Additional reading on how generics and generics inference work here

Upvotes: 2

n9iels
n9iels

Reputation: 895

In this case, the generic T isn't really useful indeed. However, assume we change the code to this:

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

When given an input, the compiler will understand that the output of this function will be a promise of the type of the parameter timout. This helps writing code that is typesafe, but can be written more "generic"

A generic is used if the type of the value is not important for the context. For example, say we crate a function that puts the generic parameter in an array. We won't need the type of this parameter, since we only put it in an array. With the generic we can tell typescript that this function accepts some sort of parameter, and the output will be an array of the same type.

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

At compile time, the compiler will figure this out. The code below will result in an error saying that toArray returns an string[], but the variable is expecting an numer[]

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

Upvotes: -1

Jared Smith
Jared Smith

Reputation: 21926

A Promise isn't really interesting by itself, any more than an empty Array is interesting by itself. It's just a placeholder for an eventual value, a box to contain, and that value has a type. We could write the same function 15 times for various types that can go in the box, or we can say to the compiler that either the caller provides the type, or it can try to infer the type from usage.

To give the intuition, let's write a function that returns the last element of an array, first in plain JS:

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

and add the types:

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

We don't really care what the type is, and we want this to work for arrays of all sorts of things:

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]); // heterogenous array, explicit

Nobody wants to sit and write out overloads for all the different things that can go in an array, so... yeah generics FTW.

Upvotes: 2

Related Questions