Angular Http Tips For Success

RxJS Map to the rescue!

David Pine

5 minute read

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 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.

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 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.

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


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 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!

comments powered by Disqus