npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ts-valueobjects

v2.1.0

Published

Typescript typesafe Value Objects and Domain Value Objects made easy

Downloads

82

Readme

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