Git Product home page Git Product logo

ts-valueobjects's Introduction

Build Coverage Status Mutation testing badge

Core principles

This package is built around 3 core principles:

  1. Value objects MUST be immutable
  2. Value objects holding the same values MUST be considered equal when compared
  3. Domain value objects should be pure typesafe types

The first 2 are commonly understood principles applied to value objects. The third is an additional principle that allows code to better express a domain's ubiqutous language, it is provided as an aditional feature see the section on Domain Value Objects below.

For a deeper understanding of value objects read Martin Fowler's article on the subject, or Eric Evans, or Vaugn Vernon's writings on Domain Driven Design.

Inspiration

This package is heavily inspiried by funeralzone/valueobjects and the accompanying blog post

ValueObjectInterface

The core of the library is the ValueObjectInterface, it enforces immutability by making the underlying value readonly and provides an interface for deserialising (toNative()) and comparing (isSame()) value objects. It requires a generic type that the vlaue being stored will be:

import { ValueObjectInterface } from "ts-valueobjects";

const anEmailAddress: ValueObjectInterface<string> = {
  value: '[email protected]',
  isSame(object: ValueObjectInterface<string>): boolean {
    return object.value === this.value;
  },
  toNative(): string {
    return this.value;
  }
}

// trying to reset the value will give an error:
anEmailAddress.value = '[email protected]'
 -> `Cannot assign to 'value' because it is a read-only property`

More complex value objects can be created and compared depending on the relevant properties that define their equality:

const someCoordinate: ValueObjectInterface<{x:number, y:number}> = {
  value: {
      x: 3.2,
      y: 1.4,
  },
  isSame(object: ValueObjectInterface<string>): boolean {
    return object.x === this.x && object.y === this.y;
  },
  toNative(): {x:number, y:number} {
    return this.value;
  }
}

Type helper classes

Creating lots of value objects this way can get verbose so you can use some of the included classes for creating common scalar types (StringScalar, FloatScalar, IntegerScalar, BooleanScalar, NullScalar).

StringScalar

import { StringScalar } from "ts-valueobjects";

// creating the above email example is much easier:
const anEmailAddress = new StringScalar('[email protected]');
// anEmailAddress is now immutable:
anEmailAddress.value = '[email protected]'
 -> Will give a compiler error: `Cannot assign to 'value' because it is a read-only property`

// or using the additional static helper:
const anotherEmailAddress = StringScalar.fromNative('[email protected]');

console.log(anEmailAddress.isSame(anotherEmailAddress)); // false

FloatScalar

import { FloatScalar } from "ts-valueobjects";

const floatValue = FloatScalar.fromNative(23.5);
const floatValue = new FloatScalar(23.5);

floatValue.isNull();
floatValue.isSame(...);
floatValue.toNative();

BooleanScalar

import { BooleanScalar } from "ts-valueobjects";

const boolValue = BooleanScalar.true();
const boolValue = BooleanScalar.false();
const boolValue = BooleanScalar.fromNatiave(true);
const boolValue = new BooleanScalar(true);

boolValue.isNull();
boolValue.isSame(...);
boolValue.toNative();
boolValue.isTrue();
boolValue.isFalse();

IntegerScalar

import { IntegerScalar } from "ts-valueobjects";

const integerValue = IntegerScalar.fromNative(BigInt(1));
const integerValue = new IntegerScalar(BigInt(1));

integerValue.isNull();
integerValue.isSame(...);
integerValue.toNative();

NullScalar

import { NullScalar } from "ts-valueobjects";

const nullValue = NullScalar.fromNative();
const nullValue = new NullScalar();

integerValue.isNull();
integerValue.isSame(...);
integerValue.toNative();

Enum Type Helper

Using the helper for cretaing Enums will throw errors when trying to access properties that do not exist:

import { Enumerate, EnumValueObject } from "ts-valueobjects";

class Enumerated extends Enumerate(
  class extends EnumValueObject {
    static VAL1 = "One";
    static VAL2 = "Two";
  }
) {}
const value = new Enumerated(Enumerated.VAL3); // will throw an error
const value = new Enumerated(Enumerated.VAL1); // ok
// or
const value = Enumerated.fromNative(Enumerated.VAL1); // ok

Composite Value Objects

The CompositeValueObject allows you to create value objects that are more complex and contain any number of other value objects (including nested CompositeValueObjects and Domain Objects).

import { CompositeValueObject } from "ts-valueobjects";

class User extends CompositeValueObject<{
  name: StringScalar;
  email: StringScalar;
  isRegistered: BooleanScalar;
}> {
  constructor(name: StringScalar, email: StrigScalar, isRegistered: BooleanScalar) {
    super({
      name,
      email,
      isRegistered
    });
  }

  public static fromNative(value: { name: string; email: string, isRegistered: boolean }): User {
    return new this(
      StringScalar.fromNative(value.name),
      StringScalar.fromNative(value.email),
      BooleanScalar.fromNative(value.isRegistered)
    );
  }

  public getName = (): StringScalar => {
    return this.value.name;
  };

  ...
}

// immutability of the properties is still enforced:
const user = new User(...);
user.value.name = StringValue.fromNative('new name'); // -> this will throw a TypeError

Domain Value Objects

The above helpers can be combined with the DomainObjectFrom() mixin to allow you to easily create typesafe domain value objects that are more expressive of your domain language. For example:

import { StringScalar, DomainObjectFrom } from "ts-valueobjects";

class EmailAddress extends DomainObjectFrom(
  // the class to extend the domain object from
  class extends StringScalar {
    // a required property that gives this object it's uniqueness 
    // allowing type checking in other parts of the application
    readonly EmailAddress = true;
  }
) {}

class PersonName extends DomainObjectFrom(
  // the class to extend the domain object from
  class extends StringScalar {
    // a required property that gives this object it's uniqueness 
    // allowing type checking in other parts of the application
    readonly PersonName = true;
  }
) {}

class User {
  name: PersonName;
  email: EmailAddress;

  // the compiler will complain if you try and pass anything other than a PersonName 
  // or EmailAddress to this constructor
  constructor(name: PersonName, email: EmailAddress) {
    this.name = name;
    this.email = email;
  }
}

const user = new User(
  new EmailAddress('[email protected]');
  new PersonName("Papa John");
);

You can further extend these domain objects to add any domain specific logic you would like:

class EmailAddress extends DomainObjectFrom(
  class extends StringScalar {
    readonly EmailAddress = true;

    getRootDomain = (): string => {
      return this.value.split('@')[1];
    }
  }
) { }

Or provide further validation on the instantiation of the object:

class EmailAddress extends DomainObjectFrom(
  class extends StringScalar {
    readonly EmailAddress = true;

    constructor(value: string) {
      super(value);
      if (value.length <= 3) {
        throw Error("Invalid email address");
      }
    }
  }
) { }

Nullable Value Objects

The abstract NullableValueObject class allows wrapping a null and a non-null implementation into the same interface as a ValueObjectInterface. You just have to define 3 static methods: fromNative() which does the null / non-null negotiation, and, nonNullImplementation() and nullImplementation() which return the relevant implementations for the non-null and the null conditions. These methods should each return a ValueObjectInterface. By default NullableValueObject includes a nullImplementation() that returns a NullScalar. However this can be overridden and return any ValueObjectInterface implementation you like.

import { NullableValueObject, NullOr, StringScalar } from "ts-valueobjects";

class NullableUserName extends NullableValueObject<string> {
  public static fromNative(value: NullOr<string>): NullableUserName {
    return new this(this.getWhichNullImplementation(value));
  }

  public static nonNullImplementation(value: string): StringScalar {
    return new StringScalar("fixed string");
  }
}

const nullVersion = NullableUserName.fromNative(null);
console.log(nullVersion.isNull()) // -> true

const nonNullVersion = NullableUserName.fromNative("John Doe");
console.log(nonNullVersion.isNull()) // -> false

console.log(nonNullVersion.isSame(nullVersion)) // -> false

Optionally override the default nullImplementation():

class NullImplementationValueObject extends ValueObject<null> {
  ...
}

class NullableUserName extends NullableValueObject<string> {
  public static fromNative(value: NullOr<string>): NullableUserName {
    return new this(this.getWhichNullImplementation(value));
  }

  public static nullImplementation(): StringScalar {
    return new NullImplementationValueObject();
  }

  public static nonNullImplementation(value: string): StringScalar {
    return new StringScalar("fixed string");
  }
}

ts-valueobjects's People

Contributors

kevbaldwyn avatar

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.