Coder Social home page Coder Social logo

Comments (22)

zender avatar zender commented on May 22, 2024 2

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

from class-transformer.

mustafaekim avatar mustafaekim commented on May 22, 2024 2

You may also check ta-json

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

from class-transformer.

olee avatar olee commented on May 22, 2024 2

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[];
}

from class-transformer.

mustafaekim avatar mustafaekim commented on May 22, 2024 1

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' } ] }

from class-transformer.

NoNameProvided avatar NoNameProvided commented on May 22, 2024 1

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?

from class-transformer.

Davidiusdadi avatar Davidiusdadi commented on May 22, 2024 1

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

from class-transformer.

alexpls avatar alexpls commented on May 22, 2024 1

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.

from class-transformer.

mustafaekim avatar mustafaekim commented on May 22, 2024

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

from class-transformer.

NoNameProvided avatar NoNameProvided commented on May 22, 2024

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;
    }
    
}

from class-transformer.

mustafaekim avatar mustafaekim commented on May 22, 2024

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

from class-transformer.

NoNameProvided avatar NoNameProvided commented on May 22, 2024

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.

from class-transformer.

mustafaekim avatar mustafaekim commented on May 22, 2024

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

from class-transformer.

NoNameProvided avatar NoNameProvided commented on May 22, 2024

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.

from class-transformer.

mustafaekim avatar mustafaekim commented on May 22, 2024

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

from class-transformer.

sr-hosseyni avatar sr-hosseyni commented on May 22, 2024

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

from class-transformer.

NoNameProvided avatar NoNameProvided commented on May 22, 2024

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.

from class-transformer.

sr-hosseyni avatar sr-hosseyni commented on May 22, 2024

Hi @NoNameProvided ,

Have you created branch ? I like to try it.

from class-transformer.

NoNameProvided avatar NoNameProvided commented on May 22, 2024

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.

from class-transformer.

janis91 avatar janis91 commented on May 22, 2024

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

@NoNameProvided

from class-transformer.

NoNameProvided avatar NoNameProvided commented on May 22, 2024

Thanks, @janis91! I will review it.

from class-transformer.

sergeytkachenko avatar sergeytkachenko commented on May 22, 2024

My custom wrapper - https://github.com/sergeytkachenko/typescript-serialisation/blob/main/src/examples/chart-series.example.ts

from class-transformer.

NoNameProvided avatar NoNameProvided commented on May 22, 2024

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?

from class-transformer.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.