2

I've reading up on interfaces and type assertion. Been looking at pages like:

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!

Community
  • 1
  • 1
AT82
  • 71,416
  • 24
  • 140
  • 167

3 Answers3

3

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 (:

Angel Q
  • 124
  • 10
Sasxa
  • 40,334
  • 16
  • 88
  • 102
  • Thanks for you answer. It's of course difficult to see my problem, since you can't see complete code. I can't extend my Teacher class with my interface, since students is private in the class. I had someone look at my code, and I was told to implement an interface, to sort out my problem. I'm losing data e.g when I retrieve one Teacher. There I have to map the incoming Teacher as a "new" teacher, otherwise I get a self.context error. There I can only say `new Teacher(teacher.id, teacher.name)` if you look at my teacher class. So l lose data there already. – AT82 Jun 13 '16 at 17:20
  • Added the `getTeacher` method in my service to showcase one of the problems where I am losing data. – AT82 Jun 13 '16 at 17:28
  • @Sasxa why does res.json() convert to Teacher? I am looking for some documentation that explains this. – Tom Deseyn Jun 20 '16 at 09:17
  • It doesn't convert it, it's still just a JSON. But because you expect that response will have the same structure as Teacher, you typecast it as such, and your tools and editor can help you with type checking, auto completion, debugging etc... @TomDeseyn – Sasxa Jun 20 '16 at 09:30
  • @Sasxa I don't find the cast operation. The call to map returns Observable, where is it casted to Observable? – Tom Deseyn Jun 20 '16 at 09:50
  • @TomDeseyn You decide what to put ; If you don't know or care what it is you'll use `any`, or you can be more specific. It's not language/framework feature, it's developer's choice... – Sasxa Jun 20 '16 at 10:15
  • @Sasxa see http://stackoverflow.com/questions/37919773/why-does-any-convert-to-a-more-specific-type-without-a-cast – Tom Deseyn Jun 20 '16 at 11:07
1

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));
Sasxa
  • 40,334
  • 16
  • 88
  • 102
1

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.

pcan
  • 893
  • 11
  • 24