1

My problem is pretty trivial, though I can't find an appropriate solution.

I have the following interface:

export interface User {
    id: number;
    ...
}

And method

  getCurrentUser(): Observable<User> {
    return this.http.get<User>('url');
  }

Okay, now, I want to extend a user object, that getCurrentUser method returns with additional methods.

First thing that came to my mind is to create a decorator, something like this

export class UserDecorator implements User {
    id: number;

    constructor(private user: User) {}

    someMethod() {
        ...
    }
}

Apparently, I have to use it like so

  .pipe(map((user: User) => new UserDecorator(user)))

What I don't really like in this solution, is

  • I have to copy/paste all User properties to UserDecorator, declaring User interface as class instead, to avoid copy/pasting, is not a good solution as well
  • Setting User object as a constructor argument result in the following problems
    1. I have to leave it as it is, and access User properties through additional member (e.g. userDecorator.user.id) which is not looking good
    2. I have to manually copy all the values from User to UserDecorator in the constructor.

Does my concerns make sense? Is there some better solution, or at least, some conventional solution among Angular community for that problem?

Thank you.

Majesty
  • 2,097
  • 5
  • 24
  • 55
  • For the problem 2, you could use a `BaseDecorator` class that enums and copies all the properties into this. – Andrei Tătar Jul 26 '19 at 07:47
  • @AndreiTătar yeah, this is good one, thought about it too, but I'm just starting my way in Angular and wondering, if there more elegant solution, thanks for the advice though – Majesty Jul 26 '19 at 07:58
  • Not sure I understand everything, but with inheritance (for the User objects) + generics (on the service side) you should be able to do what you want – Damien Legros Jul 26 '19 at 08:22
  • 1
    @DamienLegros I'm sorry if was unclear, the only thing I want to do is to map a User object, received from API, into TS object, so it will have all the properties defined + custom methods. Could you provide an example of how generics could be useful here? Thank you. – Majesty Jul 26 '19 at 08:36
  • 2
    instead of manual copiing yo can use: Object.assign(this, user) and remove private from constructor is you dont want to have a user attribute as well – tano Jul 26 '19 at 08:55

2 Answers2

0

I think this is an issue that we all hit at some time - a right of passage.

TypeScript interfaces simply describe a shape - compile time typing feature. Values are coerced into the data structure during development and we get compile time checking (akin to a properly typed language like C#).

In your example return this.http.get<User>('url'); the "User" simply describes the shape the observable will return to the subscriber.

At some point though when learning OOP techniques, one starts to use classes - state (data) and behaviour (methods). An object (in the OOP sense) being a self-contained entity (OOP is a whole topic in itself).

Your map solution is one way to create classes from simple JavaScript objects, by looping over an existing object (properties) or an array and doing something. In your case you are simply creating a JavaScript "class" passing each object into the constructor of that class (read up on prototypes).

The same result could be achieved in numerous other ways...a loop for example.

To answer your question - there are ways of "looping" over properties of an object to map properties to properties without having to do this manually.

Further reading:

How do I cast a JSON object to a typescript class

Robin Webb
  • 1,355
  • 1
  • 8
  • 15
0

Using inheritance and generics, you can do something like this. The properties mapping has to be adapted to your needs of course (manually or using Object.assign or any another way of doing it...)

class User {
    private name = '';
    constructor(data: any) {
        this.name = data.name;
    }
    hi() {
        return `Hi, I'm ${this.name}`;
    }
}
class Employee extends User {
    private job = '';
    constructor(data: any) {
        super(data);
        this.job = data.job
    }
    work() {
        return `Working as a ${this.job}`;
    }
}
class MyDummyService {
    private data = {
        name: "John",
        job: "bus driver"
    }
    getCurrentUser<T extends User>(u: new (data: any) => T): T {
        return new u(this.data);
    }
}

const dummyService = new MyDummyService()
const emp1 = dummyService.getCurrentUser(Employee) // will have the work method
console.log(emp1.hi(), emp1.work())

const emp2 = dummyService.getCurrentUser(User) // wont have the work method
console.log(emp2.hi() /*, emp2.work()*/)
Damien Legros
  • 519
  • 3
  • 7