The implicit types of promises

#Under_The_Hook
7 min readDec 24, 2022

Creating a Promise is a very common action. When it gets to typing declarations for Promises, then it’s started to be complex.

Promise Constructor — landed in ES6

function sleep(delay) {
new Promise((resolve, reject) => {
setTimeout(() => resolve("done"), delay)
});
}

Above we can see our sleep function that has no types. Let’s try to add:

  1. The “delay” parameter that the function gets must be a number since it should be the second parameter for setTimeout.
  2. The syntax for a promise that is resolved to “some_type” is “Promise<some_type>”, since we resolve to a string we should use “Promise<string>”.

The final version will be:

function sleep(delay: number): Promise<string> {
return new Promise<string>((resolve, reject) => {
setTimeout(() => resolve("done"), delay)
});
}

But what about the types of “resolve” and “reject” in the Promise?

I click on Promise and that brings me to the lib folder:

Well, where is resolve and reject? Browsing a bit the lib folder and see that the folder contains a few files with promise.d.ts suffixes: lib.es2015.promise.d.ts, lib.es2018.promise.d.ts, etc...

lib is a global library, which can be accessed from the global scope (i.e. without using any form of import).

I take a look at the Promise contractor in lib.es2015.promise.d.ts:

 /**
* Creates a new Promise.
* @param executor A callback used to initialize the promise. This callback is passed two arguments:
* a resolve callback used to resolve the promise with a value or the result of another promise,
* and a reject callback used to reject the promise with a provided reason or error.
*/
new <T>(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;

The Promise contractor declares <T> which is a typescripts Generics declaration. T is going to be a type declared at run-time instead of compile time.

Promise gets resolved and rejects callbacks as inputs and uses them in the “executor”. What is an executor?

executor: A function to be executed by the constructor. It receives two functions as parameters: resolveFunc and rejectFunc. Any errors thrown in the executor will cause the promise to be rejected, and the return value will be neglected.

If we take a look at the executor type:

(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void)

Since the executor gets 2 parameters, its type is divided into 2: the resolve and the reject.

  1. resolve type — a callback function that returns nothing (void) and gets value in the type of T | PromiseLike<T>.
  2. reject type — a callback function that returns nothing (void) and gets an optional value in the type of any.

We can summarise the Promise types:

Input — The input of the Promise is <T>, which is the resolve callback input’s type (the immediate one or in the future).

Output — We can see that Promise is expected to return a value in the type of Promise<T>.

Which promise.d.ts file does the code use?

I know that for every library that was written in Typescript and imported to my project, there is a Type definitions file — with the suffix “d.ts”. But what about the JavaScript Type definitions themselves?

A special declaration file “lib.d.ts” ships with every installation of TypeScript. This file contains the ambient declarations for various common JavaScript constructs present in JavaScript runtimes and the DOM. This file is automatically included in the compilation context of a TypeScript project.

(NOTE — You can exclude this file from the compilation context by specifying the --noLib compiler command line flag (or "noLib": true in tsconfig.json)).

As we see in the case of Promise, sometimes there are few d.ts files for each JavaScript function. The “Compiler target” property in tsconfig effects which files will be used:

{
"compilerOptions": {
"target": "es6",
"lib": [
"DOM",
"DOM.iterable",
"esnext"
],
...
},
}

Setting the compiler target to es6 causes the lib.d.ts to include additional ambient declarations for more modern (+es6) stuff like Promise.

Sometimes, you want to decouple the relationship between the compile target (the generated JavaScript version) and the ambient library support. A common example is the Promise function, e.g. today, you most likely want to — target es5 but still, you use the latest features like Promise. To support this you can take explicit control of lib.d.ts using the lib compiler option in tsconfig.

Note: using — lib decouples any lib magic from --target giving you better control.

You can provide this option on the command line or in tsconfig.json (recommended):

{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es6"],
...
},
}

Promise.all() — landed in ES6

The Promise.all() method takes an array of promises as input. It gets resolved when all the promises get resolved or any one of them gets rejected. It returns or array of fulfilled results or the first rejected the result.

The Promise.all() declaration from lib.es2015.d.ts:

/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;

// see: lib.es2015.iterable.d.ts
// all<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]>;

Let’s try to understand the declaration:

The input type is:

all<T extends readonly unknown[] | []>

The input value of Promise.all() is an array of Promises. Its type is a read-only array of the unknown as a generic ( T can be any type that is a subclass of readonly unknown[] | []). We can see that the array is read-only, which means that it won’t have some methods like pop, push, etc.

The input itself is:

(values: T)

We declare T in the Angle brackets.

The output type:

Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>

Looks a bit complex. Let’s try to understand step by step:

  1. The output is a Promise as we can see, but what returns this promise?
  2. Inside the angle brackets, we have the promise’s expected fulfilled type: { -readonly [P in keyof T]: Awaited<T[P]> }.
  3. The curly brackets seem like a mapped type that creates a new type by taking each property T and transforming it using the type Awaited<T[P]>. The type Awaited<T> is the type that unwraps the type of a promise if it is a promise, or otherwise returns the type T as is.
  4. whats about the readonly? In a mapped type, the read-only modifier makes all the properties of the resulting type read-only, meaning that they cannot be modified.
  5. For example, if we have the type T = { a: number, b: Promise<string> }, the type { -readonly [P in keyof T]: Awaited<T[P]> } would be equivalent to { a: number, b: string }.
  6. Overall, the type Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }> represents a promise that, when resolved, will contain an object with properties of the type T, with each property transformed using the Awaited<T[P]> type. The properties of the object will be read-only.

Promise.race() — landed in ES6

Promise.race() gets an array of promises. It stops when there is at least one resolved/rejected promise and returns the first resolved/rejected promise value.

The Promise.race() declaration from lib.es2015.d.ts:

  /**
* Creates a Promise that is resolved or rejected when any of the provided Promises are resolved
* or rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
race<T extends readonly unknown[] | []>(values: T): Promise<Awaited<T[number]>>;

// see: lib.es2015.iterable.d.ts
// race<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>>;

Let’s try to understand the declaration:

The input type is:

race<T extends readonly unknown[] | []>

The input value of Promise.race() is an array of Promises. Its type is a read-only array of the unknown as a generic ( T can be any type that is a subclass of readonly unknown[] | []). We can see that the array is read-only, which means that it won’t have some methods like pop, push, etc.

The input itself is:

(values: T)

We declare T in the Angle brackets.

The output type:

Promise<Awaited<T[number]>>

Promise<Awaited<T[number]>> is a type that represents a promise that resolves to an awaited array of elements of type T.

In TypeScript, the Promise type is used to represent a value that may not be available yet but is expected to be resolved in the future. The Awaited type is a helper type that represents the type of the resolved value of an Async type or the type of the value that is returned from an await expression.

Promise.allSettled() — landed in ES2020

Promise.allSettled() accepts an array of promises, and once all promises are either resolved or rejected then it returns an array containing all the data with resolved and rejected promises for we to have passed the promises.

The Promise.allSettled() declaration from lib.es2020.d.ts:

/**
* Creates a Promise that is resolved with an array of results when all
* of the provided Promises resolve or reject.
* @param values An array of Promises.
* @returns A new Promise.
*/
allSettled<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: PromiseSettledResult<Awaited<T[P]>> }>;

/**
* Creates a Promise that is resolved with an array of results when all
* of the provided Promises resolve or reject.
* @param values An array of Promises.
* @returns A new Promise.
*/
allSettled<T>(values: Iterable<T | PromiseLike<T>>): Promise<PromiseSettledResult<Awaited<T>>[]>;

We can see that the input type is the same as we saw in the other Promises. The diff is in the out type. If in Promise all we have:

Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>

here we have:

Promise<PromiseSettledResult<Awaited<T>>[]>

the type Promise<PromiseSettledResult<Awaited<T>>[]> represents a promise that will eventually produce an array of PromiseSettledResult objects, each of which represents the result of a promise that has either resolved or rejected.

--

--