Skip to main content
Module

x/ts_serialize/docs/polymorphism.md

A zero dependency library for serializing data
Go to Latest
File

🥣 ts_serialize

tests release github doc deno doc License: MIT

Home

Polymorphism

The @PolymorphicResolver and @PolymorphicSwitch decorators can be used to cleanly handle deserializing abstract types into their constituent implementations.

Polymorphic Resolver

The following example shows how the @PolymorphicResolver decorator can be used to directly determine the type of an abstract class implementor, which will then be used when deserializing JSON input.

enum Colour {
  RED = "RED",
  BLUE = "BLUE",
}

abstract class MyColourClass extends Serializable {
  @SerializeProperty()
  public colour?: Colour;

  @PolymorphicResolver
  public static resolvePolymorphic(input: string): MyColourClass {
    const colourClass = new PolymorphicColourClass().fromJSON(input);

    switch (colourClass.colour) {
      case Colour.RED:
        return new MyRedClass();
      case Colour.BLUE:
        return new MyBlueClass();
      default:
        throw new Error(`Unknown Colour ${colourClass.colour}`);
    }
  }
}

// Helper class to parse the input and extract the colour property's value
class PolymorphicColourClass extends MyColourClass {
}

class MyRedClass extends MyColourClass {
  @SerializeProperty()
  private crimson = false;

  public isCrimson(): boolean {
    return this.crimson;
  }
}

class MyBlueClass extends MyColourClass {
  @SerializeProperty()
  private aqua = false;

  public isAqua(): boolean {
    return this.aqua;
  }
}

// Serialize using PolymorphicColourClass to determine the value of `colour`, then serialize using `MyRedClass`
const redClass = polymorphicClassFromJSON(
  MyColourClass,
  `{"colour":"RED","crimson":true}`,
);

console.log(`Is a red class? ${redClass instanceof MyRedClass}`);
// > Is a red class? true
console.log(`Is crimson? ${(redClass as MyRedClass).isCrimson()}`);
// > Is crimson? true

Polymorphic Switch Resolver

The @PolymorphicSwitch decorator can be used as a quick way to serialize simple polymorphic types based on the properties of a child class. Unlike the @PolymorphicResolver decorator all implementors should have to do is annotate an existing property and provide an initializer.

Note that currently @PolymorphicSwitch can only be applied to child classes deserializing from their direct parent class.

enum Colour {
  RED = "RED",
  BLUE = "BLUE",
}

abstract class MyColourClass extends Serializable {
  @SerializeProperty()
  public colour?: Colour;
}

class MyRedClass extends MyColourClass {
  // Note that this static property is NOT part of serialization
  // colour will be set by MyColourClass#colour's `@SerializeProperty` annotation
  @PolymorphicSwitch(() => new MyRedClass())
  private static colour = Colour.RED;

  @SerializeProperty()
  private crimson = false;

  public isCrimson(): boolean {
    return this.crimson;
  }
}

class MyBlueClass extends MyColourClass {
  @PolymorphicSwitch(() => new MyBlueClass())
  private static colour = Colour.BLUE;

  @SerializeProperty()
  private aqua = false;

  public isAqua(): boolean {
    return this.aqua;
  }
}

// Serialize using JSON.parse, read `colour` off of the parsed object
// then parse using the value provided in `@PolymorphicSwitch`
const redClass = polymorphicClassFromJSON(
  MyColourClass,
  `{"colour":"RED","crimson":true}`,
);

console.log(`Is a red class? ${redClass instanceof MyRedClass}`);
// > Is a red class? true
console.log(`Is crimson? ${(redClass as MyRedClass).isCrimson()}`);
// > Is crimson? true

@PolymorphicSwitch is also useful on more complex polymorphic types that may not have a simple one to one property value to implementation mapping

abstract class MyColourClass extends Serializable {}

class MyRedClass extends MyColourClass {
  @PolymorphicSwitch(() => new MyRedClass(), true)
  @PolymorphicSwitch(() => new MyRedClass(), false)
  @SerializeProperty()
  private crimson = false;

  public isCrimson(): boolean {
    return this.crimson;
  }
}

class MyBlueClass extends MyColourClass {
  @PolymorphicSwitch(() => new MyBlueClass(), true)
  @PolymorphicSwitch(() => new MyBlueClass(), false)
  @SerializeProperty()
  private aqua = false;

  public isAqua(): boolean {
    return this.aqua;
  }
}

// Serialize using JSON.parse, read `crimson` off of the parsed object, then parse using the value provided in `@PolymorphicSwitch`
const redClass = polymorphicClassFromJSON(MyColourClass, `{"crimson":true}`);

console.log(`Is a red class? ${redClass instanceof MyRedClass}`);
// > Is a red class? true
console.log(`Is crimson? ${(redClass as MyRedClass).isCrimson()}`);
// > Is crimson? true

In the case a class has a property that contains a polymorphic value, a FromJSONStrategy that uses polymorphicClassFromJSON can be used to deserialize to the correct class.

abstract class PolymorphicBase extends Serializable {}

class TypeOne extends PolymorphicBase {
  @PolymorphicSwitch(() => new TypeOne(), 1)
  @SerializeProperty()
  private _type = 1;
}

class TypeTwo extends PolymorphicBase {
  @PolymorphicSwitch(() => new TypeTwo(), 2)
  @SerializeProperty()
  private _type = 2;
}

class WithPolymorphic extends Serializable {
  @SerializeProperty({
    fromJSONStrategy: (json) => polymorphicClassFromJSON(PolymorphicBase, json),
  })
  public property: PolymorphicBase;
}

const polymorphic = new WithPolymorphic().fromJSON({ _type: 2 });

console.log(polymorphic.property instanceof TypeTwo); // true