Tiny typesafe value object library for TypeScript.
- ecmascript 5
- less than 1k bytes with zero dependencies
- commonjs & es module
- typesafe and immutable class properties
- object keys filtering in runtime
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
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.
Please use npm.
$ npm install valueobject.ts
Then, use javascript module bundler like webpack or rollup to bundle this library with your code.
#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.
- This library is inspired by the prior arts below: