DEV Community

Patrice Ferlet
Patrice Ferlet

Posted on

Fixing a Promise return object to be typed in Typescript

If you decided to use Typescript as language to build a web application, you probably want to call REST API to get objects. A problem may occur when you choose the fetch standard: Promises doesn't really produce typed objects. But, there are solutions 😄

The problem

Let's image you create this class that represent a user:

class User {
   username = ""
   logout() {
      console.log("Log out!");
   }
}
Enter fullscreen mode Exit fullscreen mode

You see? There is a method to log out.

So, you say "Typescript uses typed variables, let's go to get the user from a REST API and the magic will happen"

class Api() {
  getUser(name: string): Promise<User> {
    // the problem is here
    fetch(`/api/v1/user/${name}`).then((response) => response.json())  
  } 
}
Enter fullscreen mode Exit fullscreen mode

It's a trap

The error here is to think that Typescript will cast the JSON object to a User. Even if response.json() returns any type. But…

You're wrong.

Try!

const api = new Api()
api.getUser("Franck Lambert").then((user: User) => {
 console.log("user", user); // shows a good structure
 user.logout(); // ERROR !
})
Enter fullscreen mode Exit fullscreen mode

That's a trap, I said. Even if you use typed variables, that's a JavaScript execution that's behind the scene. And TypeScript doesn't change the object type.

To understand the reason that Typescript will not create a User, just ask:

"how should it transform an anonymous object to a complete User object? What if I need to use a constructor, for example?"

In our case, there is no constructor, our class is very simple, and we can apply the JSON object to our User. So… maybe…

Maybe we can automate the construction.

There are several ways to do, but I will show you:

  • a quick and dirty method
  • a complex method

The quick and dirty solution

This method is a simple "map all to object" call. Because we don't need construction, and because it's a simple class to transport information and methods, the following can be used.

We only need to "copy" the plain object to a typed object. Of course, what you can do it like this…

fetch(`/api/v1/user/${name}`)
   .then((response) => response.json())  
   .then((user: User) => {
      const u = new User();
      u.username = user.username;
      return u;
   });
Enter fullscreen mode Exit fullscreen mode

But, let's imagine a more complex class with a lot of properties to change. And what if we add properties in the class? No… really, we require a more automated method.

And... there is one!
Object.assign(target, source) returns the target element with all source applied to it. As the target is typed, the return object is typed too.

fetch(`/api/v1/user/${name}`)
   .then((response) => response.json())  
   .then((user: User) => Object.assign(new User(), user))  
Enter fullscreen mode Exit fullscreen mode

That's quick and dirty, but that is not so bad.

And if the API returns a list of object:

fetch("/api/v1/users/")
   .then((response) => response.json())
   .then((users: Array<User>) => users.map(
       (user) => Object.assign(new User(), user))
   )  
Enter fullscreen mode Exit fullscreen mode

The complex method

There is a very nice module that you can use:

GitHub logo typestack / class-transformer

Decorator-based transformation, serialization, and deserialization between objects and classes.

class-transformer

Build Status codecov npm version

Its ES6 and Typescript era. Nowadays you are working with classes and constructor objects more than ever Class-transformer allows you to transform plain object to some instance of class and versa Also it allows to serialize / deserialize object based on criteria This tool is super useful on both frontend and backend.

Example how to use with angular 2 in plunker. Source code is available here.

Table of contents

class-transformer provides a plainToClass() function that transforms your plain objects (from JSON response, for example) to a typed object.

You will be able to do:

import { plainToClass } from 'class-transformer';

fetch(`/api/v1/user/${name}`)
   .then((response) => response.json())  
   .then((user: User) => plainToClass(User, u));
Enter fullscreen mode Exit fullscreen mode

This is OK, and it works with a complete check. So, if you have a lot of objects to treat, and you need to ensure the return type, use this module.

Is it correct?

It is for simple objects. When the class has no properties which are object instance. This only will not work on inherited classes. So you can use Object.assign when you get simple objects from API.

You'll need more work to fix embedded objects, and sometimes you'll need to build the object yourself without automation.

There is no magic, you need to develop.

If you require a lot of control, the class-transformer module is very efficient and is well-supported in all frameworks I use (Vue, Angular, etc.) in Typescript.

Hope that my article helps you.

Discussion (0)