Skip to content

Tiny type-safe value object library for TypeScript

License

Notifications You must be signed in to change notification settings

kzok/valueobject.ts

Repository files navigation

@kzok/valueobject-ts


Tiny typesafe value object library for TypeScript.

CircleCI codecov

Features

  • ecmascript 5
  • less than 1k bytes with zero dependencies
  • commonjs & es module
  • typesafe and immutable class properties
  • object keys filtering in runtime

Brief example

import {valueObjectClass} from "@kzok/valueobject-ts";

type PersonProps = {
    name: string;
    age: number;
};

const personKeys = ["name", "age"] as const;

class Person extends valueObjectClass<PersonProps>().keys(personKeys) {
    greet(): string {
        return `Hello, I am ${this.name}.`;
    }
    growOne(): Person {
        return new Person({...this, age: this.age + 1});
    }
}

const initialValue = {
    name: "Bob",
    age: 20,
    greet: null,
    growOne: () => {
        throw new Error("The method won't be overwritten!");
    },
};
const person = new Person(initialValue);

console.log(person.greet());
// "Hello, I am Bob."
console.log(person.growOne().age);
// 21

Why and when to use this?

In TypeScript, you can easily create value object with parameter properties like the following:

class Person {
    constructor(public readonly name: string, public readonly age: number) {}
    greet(): string {
        return `Hello, I am ${this.name}.`;
    }
    growOne(): Person {
        return new Person(this.name, this.age + 1);
    }
}

However, with more properties, you'll want to use named parameters like the following:

class SomeLargeValueObject {
    public readonly prop1: number | null;
    public readonly prop2: number | null;
    public readonly prop3: number | null;
    /**
     * ... more props ...
     */
    constructor(args: {
        prop1: number | null;
        prop2: number | null;
        prop3: number | null;
        /**
         * ... more props ...
         */
    }) {
        this.prop1 = arg.prop1;
        this.prop2 = arg.prop2;
        this.prop3 = arg.prop3;
        /**
         * ... more assingments ...
         */
    }
}

With many properties, this approach is frustrating. So many prior value object libraries introduce following approach.

interface ValueObjectConstructor<T extends {[k: string]: any}> {
    new (initialValue: T): Readonly<T>;
}

const valueObject = <T extends {[k: string]: any}>(): ValueObjectConstructor<T> => {
    return class {
        constructor(arg: T) {
            Object.assign(this, arg);
        }
    } as any;
};

//-----------------

interface SomeLargeValueData {
    prop1: number | null;
    prop2: number | null;
    prop3: number | null;
    /**
     * ... more props ...
     */
}

class SomeLargeValueObject extends valueObject<SomeLargeValueData>() {
    isValid(): boolean {
        /** ... */
    }
}

In TypeScript, however, this approach has a problem. Because TypeScript doesn't have Exact Type, runtime error occurs in a following case.

const passedData = {
    prop1: 1,
    prop2: 2,
    prop3: 3,
    /**
     * ... more props ...
     */
    // Oops! This will overwrite the class method!
    isValid: true,
    /**
     * ... some more other props for other usecases ...
     */
};

const nextValueObject = new SomeLargeValueObject(passedData);

//-----------------

// TypeError: isValid is not a function
if (nextValueObject.isValid()) {
    /** ... */
}

Because of that, this library filters constructor argument's property keys.

Installation

Please use npm.

$ npm install valueobject.ts

Then, use javascript module bundler like webpack or rollup to bundle this library with your code.

API reference

#function valueObjectClass()

Returns object with a property keys which is the function that takes array of property keys to filter and return the base class of the value object.

Credits