1

I'm using Angular 14. I have a number of calls in my application that return my model via HttpClient GET and POST requests:

  constructor(
    http: HttpClient, 
    ...
  ) {}

    ...
    return this.http.post<MyObject[]>(`${this.entitiesUrl}search`, searchObj)

I'm noticing that when these requests are made, the constructor of the model isn't called. I'm looking for a way to apply a function to some of the fields of my newly-built object before it is used by other parts of the application. Applying a "map" to the result of HttpClient calls is certainly an option, but there are so many, I would have to apply the same map dozens of times, which seems wasteful and poor coding practice.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Dave
  • 15,639
  • 133
  • 442
  • 830
  • 1
    Use an interceptor? –  Feb 15 '23 at 23:35
  • 1
    you could indeed use an interceptor which would map data for every call, but that behavior is unobvious for code readers and making a generic wrapper logic is not that easy sometimes. I would recommend building a service or a set of services(maybe several services inheriting some abstract superclass) which you would use instead of httpClient directly – Andrei Feb 15 '23 at 23:45
  • 2
    "several services" for the same purpose puts OP back in the issue of "wasteful and poor coding practices". Interceptors are standard practice and only non-obvious to the inexperienced coder. And that's what comments are for. –  Feb 16 '23 at 12:04
  • I'm interested in "several services" for diffeernt purposes. – Dave Feb 21 '23 at 13:49
  • You have fundamentally misunderstood what TypeScript is doing. It **does not exist** at runtime, the type metadata is erased in transpilation (because otherwise the result _wouldn't be JavaScript_). Note the explicit call out [in the documentation](https://angular.io/guide/http#requesting-a-typed-response) (originally added in response to [this issue](https://github.com/angular/angular/issues/25401)), which should be sufficiently reputable. – jonrsharpe Feb 25 '23 at 17:02

2 Answers2

1

In your example MyObject[] is just a hint, in reality there is something like JSON.parse(body) as MyObject[]. It also means that you can not have any methods in your model because they will not exist on deserialized objects.

It really depends on what exactly you want to achieve.

If you want to alter some fields by its name or type or value pattern, or inject the same set of methods then interceptor is ok. You can even have several interceptors and enable/disable them using fake headers in the request. If you don't use interceptors and don't plan to use interceptors then you can wrap HttpClient into custom service that simply appends pipe(map(x => ...)) to post/get methods.

Example: Parse date with Angular 4.3 HttpClient

If you want to convert deserialized objects into instances of concrete classes then it is not possible in some generic way, you will have to implement all the mapping, custom constructors, class registry/factory, discriminators and etc.

Alternative option is to use some sort of client code generation (e.g. OpenApi or Swagger). Usually there is a way to alter template that is used to generate code, so you can append custom code that does dates conversion and it will be automatically applied to all services.

kemsky
  • 14,727
  • 3
  • 32
  • 51
  • Regarding "Alternative option is to use some sort of client code generation (e.g. OpenApi or Swagger) ...", the API I'm consuming is OpenAPI 3.0/Swagger generated but i'm not sure how that solves the problem I've posed (making sure instances of my object's "Date" field are actually Dates) – Dave Feb 21 '23 at 19:41
  • It allows you to modify template used to generate services and inject code that does dates conversion so it will be applied to all services automatically. I think this is overkill in this case, there are plenty examples of how to create interceptor that does dates conversion even on this site. – kemsky Feb 22 '23 at 13:51
  • I've added link to the relevant answer. – kemsky Feb 22 '23 at 13:53
  • If I'm reading your link correctly, you are suggesting to apply a "map" each time an http call is made that retrieves the object. If I have several calls that retrieve the same object, I would need to apply the map each time. This seems like code duplication. I would prefer to have the code for setting the proper Date type in a single place so future callers of http methods don't haev to worry about applying a map. – Dave Feb 24 '23 at 17:27
  • Map is applied to the result of http call, so if you make 100 calls then map is applied 100 times to convert each response. Code duplication is when you actually have to write the same code again and again, but it is not the case, you have single map operator in the interceptor that converts each intercepted response. – kemsky Feb 24 '23 at 18:39
1

What you are asking for is to get the benefits of TypeScript type safety, but you do not want to implement the heavy lift required to gain those benefits.

In TypeScript, types are not strictly enforced without extra steps. You can take an interface and apply it to a response to gain the benefits of dot notation, and "implied" type checking via the IDE during development, however, you will quickly realize that the types of that interface can be violated by the API... this is a "good faith" data contract implementation, and, you are using it as an implied shape to your data response and trusting the API to not violate it.

If you want to go a step beyond this, you will need to use a class pattern that implements an interface via constructor to "enforce" explicit type checking as the class instance is created... there is no other way that I know of to to gain this benefit... and it requires you to use a map implementation to loop over the response and map the data as you have already pointed out.

There is information you will know at the time of the API call that can be used during the response for classifying the response shape into the respective model class... you can certainly "abstract" this away in to "several services" by making the services API/model specific, but in the end, no matter the implementation you choose, you will have to implement classification/mapping logic in some way/at some level to gain type safety for your API responses.

True type safety in TypeScript only exists at runtime by mapping responses to class objects (run time) that implement explicit data type checking via the constructor.


Additional info on how TypeScript behaves: Runtime typesafety in typescript

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Marshal
  • 10,499
  • 2
  • 34
  • 53