I'm passing an object from Java to Typescript that has "Map" properties. My typescript models are described by interfaces, not classes:
export interface Foo {
bar: Map<string, Bar>;
mapOfBar: Map<string, Map<string, Bar>>;
}
export interface Bar {
someField: string;
}
I wrote code to query and convert the JSON object to Foo:
import {HttpClient} from '#angular/common/http';
...
export class FooService {
constructor(private http: HttpClient) {
}
getFoo(): Observable<Foo> {
return this.http.get<Foo>('/some/route/to/FooEndpoint');
}
}
This returns me the object, but bar and mapOfBar are of type object, not Map. So after quite a bit of searching the internet I came up with this code:
getFoo(): Observable<Foo> {
return this.http
.get<Foo>('/some/route/to/FooEndpoint')
.pipe(
map((res: Foo) => {
res.bar = new Map(Object.entries(res.bar));
let actualMapOfBar = new Map(Object.entries(res.mapOfBar));
res.mapOfBar = new Map();
for (let [key, bar] of actualMapOfBar) {
res.mapOfBar.set(key, new Map(Object.entries(bar)));
}
return res;
})
);
}
This returns me bar and mapOfBar as Maps, which is great.
Call me a perfectionist, but somehow this feels clunky and wrong:
The line map((res: Foo) => { indicates that res is already implementing the Foo interface, but it doesn't - it only contains the properties, but not the correct types (yet).
This is quite a bit of conversion logic to set up, especially when dealing with deeply nested object trees.
Is there no easier way to implement this or can this be optimised? Is there ideally some "automagical" way of using the Typescript interface to convert the object?
(Angular 9.1, Typescript 3.8, rxjs 6.5)
EDIT:
This is an example response return by the endpoint:
{"bar":{"key1":{"someField":"A"},"key2":{"someField":"B"},"key3":{"someField":"C"}},"mapOfBar":{"A":{"key1":{"someField":"A"},"key2":{"someField":"B"},"key3":{"someField":"C"}},"B":{"key1":{"someField":"A"},"key2":{"someField":"B"},"key3":{"someField":"C"}},"C":{"key1":{"someField":"A"},"key2":{"someField":"B"},"key3":{"someField":"C"}}}}
No there is not. The line map((res: Foo) is -you- telling the compiler: "hey, this is of type 'Foo'", and the compiler accepts this, because he trusts you. However, you lied to the compiler, because the response of the API is just a plain JSON object. This cannot includes Map or any other class.
Like I said before, there is no automatic way to do the conversion. You will have to do this yourself. Which means, to make it type safe, you should have two interfaces. One being the FooResponse and one into which you want this Foo to be converted to. The reason this does not exist, is because TypeScript types only exist at compile time. During runtime, so when you digest the API, the types are lost, and so is your information for conversion. That's why you have to do it manually.
A small question. Why do you want them to be maps? Besides some relatively convenient API, you can also just keep it as the object it is, and change your typing to:
export interface Foo {
bar: Record<string, Bar>;
mapOfBar: Record<string, Record<string, Bar>>;
}
export interface Bar {
someField: string;
}
Is there no easier way to implement this or can this be optimised? Is there ideally some "automagical" way of using the Typescript interface to convert the object?
Yes, it is! It's called tapi.js - it's a lightweight automapper to/from JSON data.
npm i -D tapi.js
Then you can map your object in a number of ways, namely:
let typedObject = new YourClass().fromJSON(jsonData);
Or, more simply, you can map the contents of a promise like so:
http.YOUR_REQUEST_CODE_HERE
.as(YourClass)
.[then/pipe/whatever](typedObject => { ... })
You can read the docs for more info.
Related
Specifically, I'm trying to improve deserialization in my Angular project. I have a base HTTP class that abstracts away some of the universal aspects of interacting with my REST service:
import { HttpClient, HttpParams } from '#angular/common/http';
export class BaseHttpService {
...
protected get<T>(url: string, params: object = {}, options: object = {}): Observable<any> {
// some operations every get request needs to do
return this.httpClient.get<T>(url, { params: httpParams, ...options });
}
...
And I'm using the cerialize library to handle object deserialization, specifically for Java Date (millisecond since epoch format) conversion to Javascript Date, and occasionally some helper methods that I want stored on the class (as opposed to just taking the JSON in through a Typescript interface). So my use of this method always looks something like:
import { Deserialize } from 'cerialize';
import { ModelA } from './models';
export class SomeControllerService {
...
constructor(private baseHttpService: BaseHttpService) {}
someMethod<ModelA>(): Observable<ModelA> {
return this.get<ModelA>().pipe(map(response => Deserialize(response, ModelA)));
}
...
What I want to do is remove the need for .pipe(map(... which ends up being in every single method that returns a REST response object and is basically the same, just with a different model depending on the endpoint. In Java, I believe you could use the generic in the base service as an input to the Deserialize function like (response => Deserialize(response, T.class)). I also want to avoid adding another parameter to the base get function. Per my title, I could also see a solution that refactors to calling a function on the generic, that the individual models could implement specific deserialization logic in, but again, since the generic is just a type, that doesn't seem possible. Am I just running up to a limitation of Typescript?
I am pulling weather data from an API that sends a JSON object. Now I want to do something with this object and access one of its properties. But of course, the IDE doesn't know the Object structure and it tells me that the Property 'x' doesn't exist on type 'object'.
Nevertheless in practice the code works of course.
But is there any way to deal with this problem?
Without seeing your code, I think you could apply 3 different approaches:
1) a simple solution is for you to use a generic T type:
public getJSON<T>(): Observable<T> {
return this.http.get<T>(url).pipe(
tap(data => console.log(`JSON::get (tap)\n\tdata: %o`, data)),
catchError(err => console.log(err))
);
}
You could define an interface like this:
export interface GenericServerResponse {
[x: string]: any
}
And use this interface in your component.ts I guess.
public componentData: GenericServerResponse = null;
However this kind of approach takes away the fun of having typescript in the project.
2) You could also use union types, so that for example the service response could be of different types like so:
public getJSON(): Observable<UserModel | AdminModel | SuperUserModel> {
return this.http.get<UserModel | AdminModel | SuperUserModel>(url).pipe(
...
}
Or better since we will be reusing this, declare the type:
export type ConfigModel = UserModel | AdminModel | SuperUserModel;
public getJSON(): Observable<ConfigModel>
This is simple and more solid, but you will need to know the different models you could receive.
3) This is how I think you should do it, since you must have the weather APIs full response, map the whole model 4 example:
export interface WeatherModel {
temp: number;
wind: number;
cloudCoverage: number;
city: {
id: number;
name: string;
};
... --> the rest of the model from the API, you don't necessarily have to put all the API's attributes, only the ones you use in your app.
}
Then you just need to use the Partial utility type:
public getJSON(): Observable<Partial<WeatherModel>> {
return this.http.get<Partial<WeatherModel>>(url).pipe(
...
}
Partial makes every property of the interface optional. This way the IDE will be happy and show you the object attributes.
And if you need to type data in the component just do the same with Partial:
public componentData$: Observable<Partial<WeatherModel>>;
...
this.componentData$ = this.httpService.getJSON().subscribe(
(resp: Partial<WeatherModel>) => console.log(resp.city.name)
);
Task.ts:
export class Task {
name: string;
dueDate: Date;
}
tasks.service.ts:
#Injectable()
export class TasksService {
constructor(private http: HttpClient) { }
getTasks(): Observable<Task[]> {
return this.http.get<Task[]>(`${WEBAPI_URL}/Tasks`);
}
}
The Task objects I get back from getTasks() have their dueDate field assigned but the value is of type string instead of Date like I would expect.
Some searching lead me to this issue on the Angular github which made clear to me that HttpClient has no intent of properly parsing my object. Unfortunately the issue didn't give helpful guidance about what I should actually be doing to get my Date object. What do I do?
You have several options here.
1) You can deal with the date as a string in the UI. So change the definition of Task to retain the date as a string and work with it that way, converting it to a date for calculations as needed.
2) You can map each object coming back from the Http request to a Task object. Something like this:
getTasks(): Observable<Task[]> {
return this.http.get<Task[]>(`${WEBAPI_URL}/Tasks`)
.pipe(
map(items => {
const tasks: Task[] = [];
return items.map(
item => {
item.dueDate = new Date(item.dueDate);
return Object.assign(new Task(), item);
});
}),
tap(data => console.log(JSON.stringify(data))),
catchError(this.handleError)
);
}
This also have the benefit of having actual task objects in your array, meaning that if you ever add Task methods or getters/setters they will be correctly associated with your tasks array.
EDIT:
It may be better to build a utility class that handled the serialization/deserialization of your objects. Then the above code would look more like this:
getTasks(): Observable<Task[]> {
return this.http.get<Task[]>(this.url)
.pipe(
map(TaskSerializer.serialize),
catchError(this.handleError)
);
}
declare it as a date in the component like this:
example.component.ts
constructor(private taskService: TaskService) {
}
ngOnInit() {
this.taskService.getTaksks().subscribe(response => {
tempValue = response.body;
tempValue.dueDate = new Date(tempValue.dueDate.format('MM-DD-YYYY');
});
}
OR save it as an instant
Task.ts
export class Task {
name: string;
dueDate: Instant;
}
I would suggest doing the first way. I would also suggest looking at the moment.js library
EDIT: I would declare it as a Date object and let it store it as a string on the db. That is how I have seen most use cases for dealing with dates and it is how everyone in my company has dealt with dates. 99% of the time you just want the month/day/year so it makes sense that you will store only that on the db, but it is a little cumbersome to format it to a date object on the ui side.
I found a not so heavy interceptor, allowing to have directly correct Date in objects when using HTTP calls in Angular. You can find it here: https://dev.to/imben1109/date-handling-in-angular-application-part-2-angular-http-client-and-ngx-datepicker-3fna
Please note that the only thing I had to change was the regex, in order to make the Z at the end optional and I use the DateTimeFormatter.ISO_DATE_TIME format on server side.
I am creating an HTTP request to the back end thru this angular implementation.
I prepared the app.module by first importing the {HttpClientModule} from '#angular/common/http' into the app.module and then adding it to the imports array making it available to the rest of the app.
then, I use the get method on HttpClient to access the data in my server
That's the game plan.
The question is about determining the data type to be returned by the server.
The component has this code... I cut the not-needed stuff for brevity.
#Component(...)
export class MyComponent implements OnInit {
// results: string[]; // I am not sure about this
constructor(private http: HttpClient) {}
ngOnInit(): void {
url = 'http://example.com/authentication/get-username.php';
http
.get<MyJsonData>(url)
.subscribe...
}
My question is..
What should I type-cast where it says ? in the above code?
Little bit more backgrond:
the URL returns something like this:
{"wordpress_username":"admin_joe","user_role":"admin"}
or if no user logged in, this
{"wordpress_username":"","user_role":""}
now, here, we got a a format like {"x":"y", "j":"k"}
this is a json string. but, it is also an object
And this is where my confusion starts.
Would it be better to build an interface for this?
If so, what would be the interface look like?
Using Typescript is preferred for its strong typing also on the level of the Typescript. It is better to have a type for that.
In both cases you have the same shape of your object with two properties wordpress_username and user_role. Your interface can look like this
export interface MyJsonData {
wordpress_username: string,
user_role: string
}
Also if you can have a response with only one property, you can make them optional appending ? at the end of the property name
export interface MyJsonData {
wordpress_username?: string,
user_role?: string
}
I've reading up on interfaces and type assertion. Been looking at pages like:
Typescript parse json with class and interface
How do I cast a json object to a typescript class (this one has nothing to do with TS!)
Parse complex json objects with Typescript
TS doc about interfaces
I'm getting the hang of it, and it's pretty straightforward in understanding the basics. Though: Nowhere do I find how to type a JSON object to a object I use in my app, when HTTP is involved.
For example, I have an Array of Teacher-objects. Each Teacher has an id, name and an Array of Students. I have another class Students that contain their attributes..... etc.
Somewhere in those links I read that, unless you need to perform actions to an object, it's enough to have just an Interface. But unless you want to do actions to the Object you need a separate class?
My actual Teacher class begins with... :
export class Teacher {
private students: Array<Student>;
constructor(public id: string, public name: string) {
this.students = new Array<Student>();
}
public getStudents(): Array<Student> {
return this.students;
}
}
First of all, how would the code look like if I want to cast (or assert the type) the JS object to a Teacher object?
Now my code looks something like this:
Service:
getTeachers() {
return this.http.get('someUrl')
.map((res: Response) => res.json())
}
Component (Angular 2 Component):
export class ListComponent implements OnActivate {
id: string;
name: string;
teachers: Teacher[];
constructor(public _service: Service, public _router: Router) {
}
routerOnActivate(): void {
this._service.getTeachers()
.subscribe(teachers => this.teachers = teachers);
}
My interface would look like:
export interface TeacherJSON {
id: string,
name: string,
students: Array<Student>;
}
If the the above interface is not enough to be able to perform actions on the object, how do I move forward? I did learn that you can add methods to your interface like:
interface Thing {
a: number;
b: string;
foo(s: string, n: number): string;
}
process(x: Thing) {
return x.foo("abc");
}
I do understand the above example, but I guess the http, mapping and subscribing is throwing me off, and I don't know how to implement it in my code!
So obviously I need to first change my teachers array in the component to an instance of my interface?
There's no such thing as an instance of the interface. Interfaces are abstract, they exist in TypeScript only, they don't even compile to JavaScript. You write them to get code completion and other goodies in your editor; to help you in large apps, so you don't have to remember what properties and methods every single object has.
When you have to instantiate an object, interfaces can't do that - you'll use a class, and create properties and methods as usual. Correct way to do this in your example would be:
interface ITeacher{
id: string,
name: string,
students: Array<Student>;
};
class Teacher implements ITeacher {
id: string,
name: string,
students: Array<Student>;
}
In your case implementing interface is not necessary, TypeScript is smart enough to pull the information from classes too, so these would be the same:
teachers: Teacher[];
teachers: ITeacher[];
Creating an interface would make sense if you have several types of teachers, and you wanted to make sure each one has all necessary properties/methods:
class FirstGradeTeacher implements ITeacher{}
class SecondGradeTeacher implements ITeacher{}
You may have noticed I've not mention JSON at all. Forget about it! ...when working with your object models (classes or interfaces). It's just a data format for your logical models. When you are structuring your models, and planning how they should work, you don't care about protocols and formats (Http service handles that).
getTeachers(): Observable<Teacher> {
return this.http.get('someUrl')
.map((res: Response) => res.json())
}
This code is how you would utilize interface. You just tell TypeScipt that when you get response from someUrl and parse it as json you expect data to be of type Teacher.
Then in other places when you subscribe() to it, it can tell you that the object you're getting has id, name and students properties:
this._service.getTeachers()
.subscribe((teachers: Teacher[]) => this.teachers = teachers);
Hope this helps (:
Assuming you are getting list of teachers via http, you can filter it to get the one you need:
getById(id: string) {
return this.http.get('someUrl' + id)
.map((res: Response) => res.json())
.filter(teacher => teacher.id === id));
If I understood well your problem, you need to provide some methods to a deserialized JSON object which has Object as its prototype. At the moment, this is not possible with plain javascript API, since the content you receive from REST services is a plain string that becomes an object through JSON.parse. You should use another deserialization mechanism which is able to inspect your classes at runtime, and re-build their instances from the deserialized JSON. In this way you'll have your fresh JSON objects but with the right prototype that has all the methods for that class. I'll try to make an example.
Let's suppose you're calling a REST endpoint that gives you Teacher-like objects. At some point you do var freshObject = JSON.parse(receivedString) (actually Angular already does it, but we're going to improve this mechanism); The simplest thing to do (but not the wisest one) is to change the prototype of freshObject to Teacher prototype: Object.setPrototypeof(freshObject, Teacher.prototype). Now you can invoke Teacher methods on freshObject but this is obviously a shallow change, since the nested objects (Students in this case) still have Object as their prototype, and the problem is there again.
Now it's clear that we need something that is able to investigate the Teacher class structure recursively while re-building the received objects with the right prototypes (actually, instead changing prototypes you may build fresh copies, that should be less heavy). You may want something like: Teacher.getClass().members that will give you these information:
Name of students field
Type of students field (Array<Students> in this case)
Constructor function of the component class Student
If you have this kind of information you can recursively do the same thing we did in the previous example.
I recently released an enhanced version of the TypeScript compiler that allows this use case, I prepared a full working example for you here, and here you can find the compiler project. Let me know if this solves your problem.