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 here. 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.
ProTipNever… 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 actually 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.
See the Pen TypeScript – JSON.parse interface by David Pine (@ievangelist) on CodePen.
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.
See the Pen TypeScript – JSON.parse class with get property by David Pine (@ievangelist) on CodePen.
See the Pen TypeScript – JSON.parse class with get property by David Pine (@ievangelist) on CodePen.
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
TheObject.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
See the Pen TypeScript – JSON.parse class .ctor Object.assign by David Pine (@ievangelist) on CodePen.
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 data = 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!
About the Author:
David Pine is a Technical Evangelist and Microsoft MVP working at Centare in Wisconsin. David loves knowledge sharing with the technical community and speaks internationally at meetups, user groups, and technical conferences. David is passionate about sharing his thoughts through writing as well and actively maintains a blog at davidpine.net. David’s posts have been featured on ASP.NET, MSDN Web-Dev, MSDN .NET and Dot Net Curry. David loves contributing to open-source projects and stackoverflow.com as another means of giving back to the community. David sat on the technical board and served as one of the primary organizers of Cream City Code for going on four years. When David isn’t interacting with a keyboard, you can find him spending time with his wife and their three sons, Lyric, Londyn and Lennyx. Follow David on Twitter at @davidpine7.
Reference:
Pine, D (2018). Angular HTTP Tips for Success. Available at: https://davidpine.net/blog/angular-http-gotchas/. [Accessed 25 July 2018]