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!");
}
}
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())
}
}
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 !
})
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;
});
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 thetarget
element with allsource
applied to it. As thetarget
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))
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))
)
The complex method
There is a very nice module that you can use:
typestack / class-transformer
Decorator-based transformation, serialization, and deserialization between objects and classes.
class-transformer
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
- What is class-transformer
- Installation
- Methods
- Enforcing type-safe instance
- Working with nested objects
- Exposing getters and method return values
- Exposing properties with different names
- Skipping specific properties
- Skipping depend of operation
- Skipping all properties of the class
- Skipping private properties, or some prefixed properties
- Using groups to control excluded properties
- Using versioning to control exposed and excluded properties
- Сonverting date strings into Date…
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));
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.
Top comments (0)