Understanding complex type declaration in Typescript

426 views Asked by At

Looking at a this type declaration:

export interface Thenable<R> {
    then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Thenable<U>
    then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => void): Thenable<U>
    catch<U>(onRejected?: (error: any) => U | Thenable<U>): Thenable<U>
}

I'm aware of what it does. I can do something like this:

type MessagesSendResponse = ...
export const announce = (options: sendParameters):Thenable<MessagesSendResponse> => {
 return Promise.all([emailMessage(options),smsMessage(options)] 
}

This makes it smart enough to know that in context

const val1 = announce(..)

that val1 is a Thenable/Promise whereas in this context

const val2 = await announce(..)

val2 is of type MessagesSendResponse

My question is that I don't understand the following about the Thenable interface:

  1. What does it mean to say Thenable<U> I understand that U is a generic type, yet what does Thenable<U> mean? What's another way to write that?

  2. Somehow this definition is saying that a function returns a thenable / promise that in turn returns a generic. Yet both the interface type and the return value for both then and catch are of type Thenable<U>. It looks like its saying that it returns itself, which I suppose is right since a thenable can return another thenable, but how does it know that it know that the resolution is MessagesSemdResponse if it says it returns Thenable<U>? Is there some build in feature in the IDE for this?

I realize question 2 reflect my confusion. Any links to references appreciated, I couldn't find anything similar to this pattern.

2

There are 2 answers

0
artem On BEST ANSWER

part1:

export interface Thenable<R> {

we are defining generic interface with one type parameter R

part2:

then<U>(onFulfilled?: ...,  onRejected?: ...): ...

it has method then, which takes two parameters onFulfilled and onRejected, and the method itself is generic - it depends on another type paramerer U.

part3, the most interesting one:

onFulfilled is declared with this type:

(value: R) => U | Thenable<U>

it means that it's either a function taking R and returning U, or another Thenable<U>

Here is a relationship between R and U: if you have Thenable<R>, it's then method accepts a callback which is called with a value of type R (the one that original thenable produced), and should return U. Or it can accept another Thenable<U>.

The return value of this then method, then, is Thenable<U>.

In short, this is how chaining of promises is described with types.

2
Nitzan Tomer On

Thenable<T> means that you have an object from which you can obtain a value of type T.
Or, if naming it Promise<T> you get a promise to a value of type T.

Because promises are usually used for asynchronous operations, the promise doesn't "return a value" but gives you an api for getting a reference to that value when it is available.

The reason that then/catch return Thenable<T> is so that you can chain the calls:

announce(...).then(value => {
    ...
}).catch(error => {
    ...
});

The function that you are passing to then/catch will be called upon when the value is available or when something went wrong.

The promise that is being returned by then/catch is not the same promise that you called the function on, it's a new instance, and the compiler infers the generic type of this new promise based on the return value of the function you passed, for example:

const fn = (): Promise<string> => {
    return Promise.resolve("43"); 
}

fn().then(str => Number(str)).then(num => console.log(num))

The compiler knows that str is of type string and that num is of type number.

You can read more about this process here: promise chaining in MDN


Edit

In the signature:

then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Thenable<U> 

onFulfilled:
A function that expects a value of type R and returns either a value of type U or a Thenable to U.

onRejected:
A function that expects a value of type any and returns either a value of type U or a Thenable to U.

returns:
A instance of Thenable for a value of type U, which is the result of the execution of either onFulfilled or onRejected.