If you’re working with asynchronous code in TypeScript, you’ve probably run into Promises, an elegant solution to handling asynchronous tasks.

With their clean syntax and powerful capabilities, promises have become an integral part of modern JavaScript development. TypeScript, being a statically typed superset of JavaScript, takes the benefits of promises even further by providing enhanced type checking and improved code maintainability.

In this article, we’ll dive into the captivating world of promises in TypeScript, covering concepts, best practices, and real-life examples.

What Are Promises?

Promises are JavaScript/TypeScript’s way of managing asynchronous data. They provide functionality for creating a wrapper around code that will take some time that you don’t want the entire program to wait for, and functionality for consuming the result of that code.

You’ll see them a lot with asynchronous data fetching, i.e. making REST API calls with something like Axios.

Promises start off with the Promise constructor:

const promise = new Promise(function(resolve, reject) {
  // code
});

The constructor takes an executor function, which takes two parameters; a function “resolve” to call when your promise has finished executing, and a function “reject” to call if the promise fails. Your promise will start executing whenever you create the promise.

Let’s take a look at a simple promise that resolves after 1 second:

const promise = new Promise(function (resolve, reject) {
    setTimeout(() => {
        resolve('Resolved!');
    }, 1000);
});

If we try to log our promise before it’s finished, we get this:

Before the promise is resolved, it will be pending. If we wait for our promise to finish with another setTimeout, we can see that it gets resolved:

In our case we know exactly when our promise will resolve, after 1 second, so we know when to read the value, but in 99% of cases, we won’t know. Thankfully, TypeScript provides functionality to run code after your promise is resolved or rejected and to get the inner value your promise returns. These are the async/await syntax, and then() syntax. Firstly let’s neaten up our promise, and turn it into a function:

function wait() {
    return new Promise<string>((resolve, reject) => {
        setTimeout(() => resolve('Resolved!'), 1000);
    });
}

Await

The await transforms your promise into synchronous code, causing your program to wait at the await until your promise resolves. It looks like this:

async function awaitSyntaxExample() {
    console.log('Not resolved');
    const result = await wait(); //result: string, since our promise is a Promise<string>
    console.log(result);
    console.log('Done!');
}

The first part is we have to mark our function as async, which allows you to await promises inside the function. Then the await keyword causes TypeScript to wait for our promise to resolve, and return the result. Async functions always return a promise, and if they don’t then the return value is automatically wrapped in a promise this means that you can also await your async function, to use it in further asynchronous code.

.then()

The other way to consume the values from promises is through .then(). These consumers attach to a promise, and are called after the promise is resolved. Here’s what that looks like:

function thenExample() {
    console.log('Not resolved');
    wait().then((result) => console.log('Definitely resolved:', result));
    console.log('Maybe resolved');
}

In our example, we get “Not resolved” first, since that line runs before our promise is even created. “Maybe resolved” could print before or after the promise is resolved; the code doesn’t wait for our promise to resolve. The callback function provided to .then() however definitely runs after our promise is resolved, and the function is passed the value the promise resolved to.

Within the callback function, the result parameter has the type of whatever our Promise does, ensuring type safety in your callback function.

The other consumers with this method are .catch() and .finally(). .finally() always runs after your promise is rejected or resolved; useful for cleanup:

function finallyExample() {
    console.log('Not resolved');
    wait()
        .then((res) => console.log('Ill run first', res))
        .then(() => console.log('Ill run second'))
        .finally(() => console.log('Ill run last'));
}

.catch() is called if your promise rejects and throws an error:

function thenExample() {
    console.log('Not resolved');
    willReject()
        .then((result) => console.log('Definitely resolved:', result))
        .then(() => console.log('I wont run'))
        .catch((e) => console.log('Rejected:', e));
    console.log('Maybe resolved');
}

Types

TypeScript provides the Promise type, which provides access to all the methods and properties you would expect:

const foo: Promise<number>;
foo.then();
await foo;

The type is generic, based on what type you want your promise to resolve to. To transform your promise from a promise to the type within, you can await the promise:

const isANumber = await foo; //const isANumber: number

If you need to access the type of the promise on a type level, TypeScript includes the handy Awaited utility type:

let stillANumber: Awaited<typeof foo> = 7;
stillANumber = await foo; //Works because they're both numbers

Useful Methods

The Promise object includes some helpful built-in methods for managing promises. These are:

Promise.all()

  • Takes an array of promises and turns them into 1 promise
  • If all the promises resolve, returns an array of all the results
  • If one rejects, rejects with the first promise’s rejection reason

Promise.allSettled()

  • The same as Promise.all(), but instead won’t reject when any of the promises rejects, and instead returns an array describing the result of each promise

Promise.any()

  • Takes an array of promises, and resolves when the first promise resolves
  • Will reject if all the promises reject, with an array of rejection reasons

Promise.race()

  • Takes an array of promises, and returns a promise that resolves when any of the promises settles, rejection or failure

Example

Let’s delve into promises in TypeScript with some real-world examples. We’ll jump into a React app, and use Promises to manage fetching data, and tracking the state of our data fetching.

Let’s set up our promise function:

type CatResponse = { url: string; _id: string };

async function fetchCat() {
    const API_PATH = 'https://cataas.com/cat?json=true';
    return axios.get<CatResponse>(API_PATH);
}

If you’re not familiar with Axios, it’s a popular library for data fetching in JavaScript/TypeScript. We’ll be performing a fetch to CatAAS, a handy API that just gives us back a random picture of a cat.

Now with that, we can fill in our component:

export default function TypescriptPromises() {

    const [status, setStatus] = useState<Status>('loading');
    const [cats, setCats] = useState<{ key: string; src: string }[]>([]);

    const map = {
        'loading': <Loading />,
        'error': <Error />,
        'success': <Cats cats={cats} />,
    } satisfies Record<Status, JSX.Element>;
    useEffect(() => {
        setStatus('loading');
        try {
            const catPromises = [...new Array(CAT_COUNT)].map(() =>
                fetchCat().then((res) => parseCatResponse(res.data)),
            );

            Promise.all(catPromises).then((res) => setCats(res)).then(() => setStatus('success'));
        } catch (e) {
            setStatus('error');
        }
    }, []);
    return (
        <div
            className='flex h-screen w-full items-center justify-center bg-neutral-950 text-neutral-50'>{map[status]}</div>
    );
}

The code here might be a little confusing, so let’s drill into it.

First we have our state:

type Status = 'loading' | 'error' | 'success';    

const [status, setStatus] = useState<Status>('loading');
const [cats, setCats] = useState<{ key: string; src: string }[]>([]);

We have one piece of state used to track the status of our API fetching, and another used to store the result.

I’m mapping each state to a component to display that reflects that state, i.e. just simple loading and error messages, and one component used to display our cat images:

    const map = {
        'loading': <Loading />,
        'error': <Error />,
        'success': <Cats cats={cats} />,
    } satisfies Record<Status, JSX.Element>;

Then we have our useEffect:

    useEffect(() => {
        setStatus('loading');
        try {
            const catPromises = [...new Array(CAT_COUNT)].map(() =>
                fetchCat().then((res) => parseCatResponse(res.data)),
            );

            Promise.all(catPromises).then((res) => setCats(res)).then(() => setStatus('success'));
        } catch (e) {
            setStatus('error');
        }
    }, []);

Firstly we set our state to loading, just before we start the API calls.

Then I’m creating my array of promises:

 const catPromises = [...new Array(CAT_COUNT)].map(() =>
                fetchCat().then((res) => parseCatResponse(res.data)),
            );

Plus using the .then() functionality to transform the API response into a slightly simpler object.

Then we can use Promise.all to fetch all of the cat images:

Promise.all(catPromises).then((res) => setCats(res)).then(() => setStatus('success'));

Once that’s complete, we can store the cats in state, and after that using another chain, we can set the state to success.

And here’s the end result:

Thanks to promises, it’s very straightforward to fetch our data, track the state of that data fetching, and keep our web app in sync with the state.

Conclusion

Thanks for reading! I hope this was a useful introduction to promises in TypeScript. If you liked this article, or if you’re having any troubles following along, feel free to leave a comment below.

Avatar photo
👋 Hey, I'm Omari Thompson-Edwards
Hey, I'm Omari! I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, reach out on Twitter at @marile0n

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.