Angular HTTP Tips For Success
RxJS Map to the rescue!
Intro
I love working with the Angular HttpClient
. It is easy to use and was designed to work with RxJS. It is vastly different from the AngularJS implementation, if you’re curious I wrote about these differences
. However, there is one common issue that developers fall victim to. The issue really relates to TypeScript generics. I have also written about generics in TypeScript
here
. But in this post, we will reveal how the issue can easily be avoided.
The problem
The problem is that the HttpClient
class exposes generic methods that allow consumers to make assumptions, these assumptions are dangerous.
ProTip Never… make… assumptions!
The assumption is that you can pass an interface
with non-primitive types or a class
as a generic type parameter, and that it will work as expected. This is simply not the case. Consider the following:
public getDetails(id: number): Promise<Details> {
return this.http
.get<Details>(`${this.baseUrl}/api/details/${id}`)
.toPromise();
}
Most of the time we’d pass in an interface
as the type parameter. Often this seems to work as the interface
is a simple property bag of primitive types. The issue is that if the interface
defines non-primitive types like a Date
or a Function
– these will not be available at runtime. Likewise, if you pass in a class
with get
or set
properties – these too will not work! The specific problem is that the underlying implementation from Angular doesn’t instantiate your object. Instead, it simply casts it as the given type. Ultimately, Angular is performing a
JSON.parse
on the body of the response.
Part of the issue is that TypeScript is blissfully unaware that Angular will not instantiate the object and treats the return as a Promise<Details>
. As such flow analysis, statement completion, and all the other amazing features that the TypeScript language services provide to your development environment work. But this is misleading because you’ll encounter runtime errors – this is the issue that TypeScript aims to solve!
Working Example
An interface
with primitive types will work just fine. The JSON.parse
will give you an object and because of JavaScript coercion, it works. TypeScript will treat it as this object and everything is perfect.
export interface Details {
score: number;
description: string;
approved: boolean;
}
Non-Working Example
Imagine that we want to return another property from the server, so we add a Date
property. Notice how our interface
added this new property. Now we want to do some date logic and use some of the methods on the Date
instance – this will not work!
export interface Details {
date: Date;
score: number;
description: string;
approved: boolean;
}
The details.date
property will exist, sure… but it will not be a Date
instance – instead it is simply a string
. If you attempt to use any of the string
methods, it will fail at runtime.
Ah, we can fix this – right?! We might think to ourselves, we’ll use a class
instead and then add a getDate()
“get function” property that will pass the .date
member to the Date
constructor. Let’s look at this.
export class Details {
date: Date;
score: number;
description: string;
approved: boolean;
get getDate(): Date {
return new Date(this.date);
}
}
Perhaps to your surprise, this doesn’t work either! The Details
type parameter is not instantiated.
If we add a constructor
to our class
and then pass in a data: any
argument, we could easily perform an Object.assign(this, data)
. This solves several issues
The
Object.assign()
method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object. MDN Web Docs
JavaScript to the rescue
What’s to stop a consumer from trying to interact with the details.date
property – if you recall it is still typed as a Date
. This is error-prone and will cause issues – if not immediately, certainly later on. Ideally, all objects that are intended to map over from JSON should contain primitive types only.
If you’re set on using a class
, you should use the RxJS map
operator.
public getDetails(id: number): Promise<Details> {
return this.http
.get<Details>(`${this.baseUrl}/api/details/${id}`)
.map(response => new Details(response.json()))
.toPromise();
}
But what if we wanted an array of details to come back – that’s easy too?!
public getDetails(): Promise<Details[]> {
return this.http
.get<Details>(`${this.baseUrl}/api/details`)
.map(response => {
const array = JSON.parse(response.json()) as any[];
const details = array.map(data => new Details(data));
return details;
})
.toPromise();
}
Types That Work Without Intervention
This table details all the primitive types that will map over without a constructor
or any other intervention.
Primitive Types | Description |
---|---|
string |
Already a string anyways |
number |
Coercion from string to number |
boolean |
Coercion from string to boolean |
array |
As long as all types are primitives also |
tuple |
Follows same rules as array |
Conclusion
While TypeScript and Angular play nicely together, at the end of the day we’re all battling JavaScript. As long as you’re aware of how your tool, framework, or technology works and why it works a certain way – you’re doing great! Take this bit of knowledge and share it with the world. If it helps you, hopefully, it will help someone else too!
Share this post
Sponsor
Twitter
Facebook
Reddit
LinkedIn
StumbleUpon
Email