Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add support for polymorphism #51

Closed
mustafaekim opened this issue Mar 25, 2017 · 23 comments
Closed

feature: add support for polymorphism #51

mustafaekim opened this issue Mar 25, 2017 · 23 comments
Labels
status: done/released Issue has been completed, no further action is needed. type: feature Issues related to new features.

Comments

@mustafaekim
Copy link

Hi, does the library support polymorphism? If so, are there any examples I can look into?

@mustafaekim
Copy link
Author

it looks like the library does not support inheritance and use of interfaces within the classes. Am I wrong?

@NoNameProvided
Copy link
Member

NoNameProvided commented Jun 2, 2017

Doesn't the following work for you?

export abstract class Base {
   @Exclude()
   @Type(() => Date)
   createdAt: Date
}

export class User extends Base {

    public id: number;
    private firstName: string;
    private lastName: string;
    private password: string;

    setName(firstName: string, lastName: string) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Expose()
    get name() {
        return this.firstName + " " + this.lastName;
    }
    
}

@mustafaekim
Copy link
Author

mustafaekim commented Jun 2, 2017

Hi, that works, but the structure below does not:

class User {
    firstName: string;
    lastName: string;
    age: number;

    @Type(() => Item)
    items: Item[];

    getFullName() { return this.firstName + " " + this.lastName; }
}

class Item {
    id: number;
    name: string;
}

class Book extends Item {
    noOfPages: number;
    public hi() { return this.id + ", " + this.name + ", " + this.noOfPages; }
}

class Toy extends Item {
    category: string;
    public hi() { return this.id + ", " + this.name + ", " + this.category;}
}

The library does not keep the "TYPE" of instance of the Item, hence, cannot go back (plain to class and then class to plain)

    it('should return plain for User with proper types', () => {
        let user: User = new User();
        user.firstName = "Mustafa";
        user.lastName = "Ekim";
        user.age = 20;

        let myBook: Book = new Book();
        myBook.id = 10;
        myBook.name = "Peter Pan";
        myBook.noOfPages = 79;

        let myToy: Toy = new Toy();
        myToy.id = 14;
        myToy.name = "cubuk";
        myToy.category = "boys";

        user.items = [myBook, myToy];

        console.log(toPlain(user))
    })

That is what I get:

User {
  firstName: 'Mustafa',
  lastName: 'Ekim',
  age: 20,
  items:
   [ Item { id: 10, name: 'Peter Pan', noOfPages: 79 },
     Item { id: 14, name: 'cubuk', category: 'boys' } ] }

@NoNameProvided
Copy link
Member

NoNameProvided commented Jun 2, 2017

Please can you format your code with three backstick (`) instead of one and also add ts to the end of the opening tag?

I dont understand what do you mean by

hence, cannot go back

Can you provide some more code examples?

@mustafaekim
Copy link
Author

Hi, did you the time to check whether polymorphic inheritance works somehow?

@zender
Copy link

zender commented Jul 13, 2017

In that case we need an discriminator option like this:

http://jmsyst.com/libs/serializer/master/reference/annotations#discriminator.

It would be very useful to have it

@NoNameProvided
Copy link
Member

Hey, sorry for the late reply.

You are right, this is not supported at the moment. While we cannot make this work "auto-magically" a Discriminator decorator or better extending the Type decorator could be a good solution.

What signature do you expect to have for such functionality?

// this is a type guard, special type in ts, 
// but basically it is a function which have to return a boolean value
function isBook(item: Book | Toy): item is Book {
    return (<Book>item).noOfPages !== undefined;
}

function isToy(item: Book | Toy): item is Toy {
    return (<Toy>item).category !== undefined;
}

// ....

class User {
    firstName: string;
    lastName: string;
    age: number;

    @Type(() => ({ [Book]: isBook,  [Toy]: isToy }))
    items: Item[];

    getFullName() { return this.firstName + " " + this.lastName; }
}

I think @Type(() => ({ [Book]: isBook, [Toy]: isToy })) is an elegant way to solve this, thoughts?

I am on vacation now, so I wont be able to look into this for a few more days, but I think it's a good thing to have, so I will pin this tab and look into this when I am back to work.

@mustafaekim
Copy link
Author

mustafaekim commented Aug 24, 2017

Hi, what you offer seems flexible to me. However if there are lots of types, then the list below will be a problem:

function isBook(item: Book | Toy | ... | ... | ... | ... ): item is Book {
    return (<Book>item).noOfPages !== undefined;
}

What mongodb does, it creates a new property when converting a class to a json file: __type
When it resolves back the model from json, it checks the __type property and instantiate it

@NoNameProvided
Copy link
Member

Hi, what you offer seems flexible to me.

I have around with it, and I had some problems with that format. Dont remember what :( But some changes will be needed to that signature.

What mongodb does, it creates a new property when converting a class to a json file: __type
When it resolves back the model from json, it checks the __type property and instantiate it

i dont like that, how about when I want to init a class from user created content? I dont want to attach a type (internal representation) to any publicly sent data.

@mustafaekim
Copy link
Author

mustafaekim commented Aug 24, 2017

It seems to me like there will be a lot of coupling with the model presentation and the code.

Similarly, how Jackson (Java JSON/Object transformer) solves like this:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "__type")
@JsonSubTypes({ @JsonSubTypes.Type(value = LinkCollector.class, name = "LinkCollector"), @JsonSubTypes.Type(value = UniqCollector.class, name = "UniqCollector"), @JsonSubTypes.Type(value = UserCollector.class, name = "UserCollector") })
public abstract class Collector implements TestResponsesContainer {
...

Above is, 1 abstract class (Collector) and 3 different classes (LinkCollector, UserCollector, UniqCollector) that implement it. The annotations define the property name: __type and the possible subtype classes. It checks the given property and use the class associated

@mustafaekim
Copy link
Author

You may also check ta-json

https://github.com/edcarroll/ta-json#jsondiscrimatorpropertypropertystring--jsondiscriminatorvaluevalueany

@sr-hosseyni
Copy link

Hi guys,
I need this feature in my personal project.
Does it ready or do you need help to developing ?

@NoNameProvided
Copy link
Member

NoNameProvided commented Dec 21, 2017

Hi @sr-hosseyni!

My first attempt to do this failed and I haven't spent any more time on it, so it definitely needs work.

@sr-hosseyni
Copy link

Hi @NoNameProvided ,

Have you created branch ? I like to try it.

@NoNameProvided
Copy link
Member

NoNameProvided commented Jan 7, 2018

Hi!

I didn't have a working solution. Also in the meantime, my Mac died so I needed to switch to another one, so I even lost what I have written, but that is not a big deal because it was nothing special or worthy anyway. I would not even near to make it work with the proposed signature and I am not even sure if it's possible that way.

@janis91 janis91 mentioned this issue Jan 26, 2018
@janis91
Copy link
Contributor

janis91 commented Jan 26, 2018

I came up with a little bit different format / way. I opened a PR for it:
#125

@NoNameProvided

@NoNameProvided
Copy link
Member

Thanks, @janis91! I will review it.

@olee
Copy link

olee commented Jun 22, 2018

I was able to write this annotation that can be used to deserialize polymorphic types.
It uses a custom transformer to replace the deserialized array after work done.
I think it should also be easy to adjust it so it can also work without arrays.
To use it, you need to provide a type checker on each subclass like this:

export abstract class Animal {
    type: string;
}

export class Dog extends Animal {
    public static typeChecker = (x => x.type === 'dog') as TypeChecker<Animal, Dog>;
    meow() { console.log('meow'); }
}

export class Cat extends Animal {
    public static typeChecker = (x => x.type === 'cat') as TypeChecker<Animal, Cat>;
    woof() { console.log('woof'); }
}
export type TypeChecker<BaseType extends object, ExtendedType extends BaseType = BaseType> = (x: BaseType) => x is ExtendedType;

export type TransformPolymorphicType<BaseType extends object, ExtendedType extends BaseType = BaseType> =
    ClassType<ExtendedType> & { typeChecker: TypeChecker<BaseType, ExtendedType> };

export type TransformPolymorphicTypeMap<BaseType extends object> =
    TransformPolymorphicType<BaseType>[];

export function TransformPolymorphic<BaseType extends object>(types: TransformPolymorphicTypeMap<BaseType>) {
    return (target: any, key: string) => {
        const metadata = new TransformMetadata(target.constructor, key, (value, obj, transformType) => {
            const src: any[] = obj[key];
            if (!src) return value;
            var executor = new TransformOperationExecutor(TransformationType.PLAIN_TO_CLASS, {});
            if (Array.isArray(src)) {
                return src.map((item, index) => {
                    const type = types.find(x => x.typeChecker(item));
                    if (!type) {
                        console.error('Could not find polymorphic type for item:', item);
                        return value[index];
                    }
                    return executor.transform(null, item, type, undefined, undefined, undefined);
                });
            } else {
                const type = types.find(x => x.typeChecker(src));
                if (!type) {
                    console.error('Could not find polymorphic type for item:', src);
                    return value;
                }
                return executor.transform(null, src, type, undefined, undefined, undefined);
            }
        }, { toClassOnly: true });
        defaultMetadataStorage.addTransformMetadata(metadata);
    };
}

Then you can use it somewhere like this:

export default class Farm {
    @TransformPolymorphic<Animal>([Dog, Cat])
    posterAnimal: Animal;

    @TransformPolymorphic<Animal>([Dog, Cat])
    animals: Animal[];
}

@Davidiusdadi
Copy link

Davidiusdadi commented Jul 31, 2018

Since i did not want to create a fork i created this way based on @olee 's approach which works without needing any class-transformer internals.

import { plainToClass, Transform } from 'class-transformer'
import { flatten } from 'lodash'
import { TransformationType } from 'class-transformer/TransformOperationExecutor'
import * as _ from 'lodash'

interface ClassType<T> { new (...args: any[]): T }

export type TypeChecker<BaseType extends object, ExtendedType extends BaseType = BaseType> = (x: BaseType) => x is ExtendedType
export type TransformPolymorphicType<BaseType extends object, ExtendedType extends BaseType = BaseType> =
    ClassType<ExtendedType> & { typeChecker: TypeChecker<BaseType, ExtendedType> }

export type TransformPolymorphicTypeMap<BaseType extends object> = TransformPolymorphicType<BaseType>[]

/**
 * Transforms simple and array properties if type's typeChecker returns true
 */
export function TypePoly<BaseType extends object>(types: TransformPolymorphicTypeMap<BaseType>) {
    return Transform((value: BaseType | BaseType[], obj, transformationType: TransformationType) => {
        const values = flatten([value]).map(v => {
            for (const type of types) {
                if (!!value && type.typeChecker(v)) {
                    return plainToClass(type, v)
                }
            }
            throw new Error('TypePoly failed to identify type of plain object')
        })
        return _.isArray(value) ? values : values[0]
    })
}

It works the same way as @olee 's.

Please see his implementation of the Dog, Cat class above.

export default class Farm {
    @TypePoly<Animal>([Dog, Cat])
    posterAnimal: Animal;

    @TypePoly<Animal>([Dog, Cat])
    animals: Animal[];
}

Depending on your needs the Type decorator may be sufficent though as it's optional parameter function already comes with enough context information to implement your own polymorphic behaviour suited to your needs. Here a simple example:

export default class Farm {
    @Type((ops)=> [opts.object[opts.property]].animaltype === 'Dog'? Dog : Cat ?)
    posterAnimal: Animal;

}

I'll stick to TypePoly until typescript has better refelection support...


Edit: I updated my previous code sample as it was buggy

@NoNameProvided NoNameProvided added type: feature Issues related to new features. flag: needs docs Issues or PRs which still need documentation added. labels Oct 26, 2018
@alexpls
Copy link

alexpls commented Apr 23, 2019

Hi @mustafaekim, have you had a chance to check out @janis91's implementation of polymorphism? I reckon since it was merged in, this issue can now be closed.

@NoNameProvided
Copy link
Member

Hi all!

I believe we already have a PR merged for polymorphism, can someone take me up to speed why these changes are required and what benefits they bring which is not provided by the current implementation?

@NoNameProvided NoNameProvided added status: awaiting answer Awaiting answer from the author of issue or PR. and removed flag: needs docs Issues or PRs which still need documentation added. labels Feb 22, 2021
@NoNameProvided NoNameProvided changed the title Polymorphism feature: add support for polymorphism Feb 22, 2021
@diffy0712 diffy0712 added status: done/released Issue has been completed, no further action is needed. and removed status: awaiting answer Awaiting answer from the author of issue or PR. labels May 15, 2024
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
status: done/released Issue has been completed, no further action is needed. type: feature Issues related to new features.
Development

No branches or pull requests

10 participants