DEV Community

Cover image for You must know how to use Generics of Promises in TypeScript
Tomohiro Yoshida
Tomohiro Yoshida

Posted on

You must know how to use Generics of Promises in TypeScript

Generics is one of the most difficult parts for TypeScript learners. However, you can not avoid studying it because you must use Generics whenever you use Promises in your code.
It's hard to make any websites or web apps without Promises. Therefore, it is a good time to learn how to use Generics of Promises if you are unfamiliar with it!

Creating Promise with the Promise class

First, create a simple Promise that returns the "message" object.

const simplePromise = new Promise((resolve) => {
    resolve({message: 'success!'});
});
Enter fullscreen mode Exit fullscreen mode

Next, execute this promise to see the result.

(async() => {
    const result = await simplePromise;
    console.log(result.message);
})();
Enter fullscreen mode Exit fullscreen mode

A success message is displayed on the console.
Okay, this code can just work. But there is a problem. Let's take a look into the type error message.

(async() => {
    const result = await simplePromise;
    // Type -> const result: unknown
    console.log(result.message);
    // Type error -> 'result' is of type 'unknown'.
})();
Enter fullscreen mode Exit fullscreen mode

Since we did not provide a type argument to the Promise class, the type of the Promise was Promise<unknown>. That is why the type checker could not predict the type of the value given by resolve. As a result, it became unknown and the type error occurred when accessing a message property.
It's very troublesome if we leave the return value as unknown. Therefore, we need to provide a type argument when creating a new Promise. Here is an updated code:

const simplePromise = new Promise<{message: string}>((resolve) => {
    resolve({message: 'success!'});
});

(async() => {
    const result = await simplePromise;
    // Type -> const result: { message: string; }
    console.log(result.message);
    // OK
})();
Enter fullscreen mode Exit fullscreen mode

Perfect! In this example code, the type checker could successfully predict the type of the result variable.

Async Functions

The type of a returned value from an async function also will be a Promise. Thus, we have to be more aware of type arguments and type annotations when working on async functions.
To examine the type of a returned value from async functions, let's make a simple fetch function and call it.

async function getRandomCatFact() {
  const response = await fetch('https://catfact.ninja/fact');
  return response.json();
}

(async() => {
    const cat = await getRandomCatFact();
    console.log(cat.fact);
})();
// [LOG]: "At 4 weeks, it is important to play with kittens so that they do not develope a fear of people." 
Enter fullscreen mode Exit fullscreen mode

Okay, this code worked and displayed the interesting fact about cats on the console. But, if it works, what's wrong?
Let's take a look at the types of the returned value.

async function getRandomCatFact() {
  const response = await fetch('https://catfact.ninja/fact');
  return response.json();
}
// Type -> function getRandomCatFact(): Promise<any>

(async() => {
    const cat = await getRandomCatFact();
    // Type -> const cat: any
    console.log(cat.fact);
})();
Enter fullscreen mode Exit fullscreen mode

Since no type argument was provided, the type of the return value from the function became Promise<any>. Thus, the type of the cat variable also became any. So, this code has a potential risk of a runtime error if a developer makes a typo like below:

console.log(cat.typo);
Enter fullscreen mode Exit fullscreen mode

The type checker cannot catch even such a silly typo if the type of the variable is any.
To solve the issue, we have to update the function so that we can pass a proper type to it. Before doing it, let's declare an interface for the API response.

interface CatFact {
  fact: string;
  length: number;
}
Enter fullscreen mode Exit fullscreen mode

And then, update the function.

async function getRandomCatFact(): Promise<CatFact> {
  const response = await fetch('https://catfact.ninja/fact');
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Thanks to the type annotation, the TypeScript type checker knows what type of value will be returned from this function. Therefore, we can see a type error if we make a typo like below:

(async() => {
    const cat = await getRandomCatFact();
    console.log(cat.typo);
    // Type error -> Property 'typo' does not exist on type 'CatFact'.
})();
Enter fullscreen mode Exit fullscreen mode

Perfect! Now, you can use Promise and Async properly in your TypeScript project.

Conclusion

In conclusion, understanding and implementing Generics with Promises is vital for TypeScript learners, as they are crucial in web development. By providing type arguments in Promises and using type annotations in async functions, developers can improve code quality, minimize runtime errors, and benefit from the type checker's ability to catch potential issues, ensuring a stable and reliable TypeScript project.

Top comments (0)