README
valueobject.ts
Tiny typesafe value object library for TypeScript.
Features
- ecmascript 5
- about 1k bytes with zero dependencies
- commonjs & es module
- typesafe and immutable class properties
- object keys filtering in runtime
- defining static type and runtime object keys at once
Brief example
import {valueObject, type, ValueType} from "valueobject.ts";
class Person extends valueObject({
name: type.string,
age: type.number,
}) {
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 as ValueType<typeof Person>);
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 the 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 has no exact type, a runtime error occurs in the following case.
const factory = (data: SomeLargeValueData): SomeLargeValueObject => {
return new SomeLargeValueObject(data);
}
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 = factory(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 valueObject(typedef)
Returns value object base class. The base class has 2 method, toJSON()
which returns plain object of its data, and equals(other)
which compare shallowly with passed parameter other. The parameter typedef's type is plain object with properties type TypeHolder
, which can create with type<T>()
function.
#function type<T>()
Returns TypeHolder
that contains type T, to create value object type definition. Its basic usage is like following.
class Comment extends valueObject({
createdAt: type<Date>(),
text: type<string>(),
parent: type<Comment | null>(),
}) {
/** ... */
}
type()
has aliases of following frequent usecases.
type.string
- equals to
type<string>()
- equals to
type.number
- equals to
type<number>()
- equals to
type.boolean
- equals to
type<boolean>()
- equals to
type.null
- equals to
type<null>()
- equals to
type.undefined
- equals to
type<undefined>()
- equals to
type.array<T>(arg?: T)
- example:
type.array(type.string)
- example:
type.optional<T>(arg?: T)
- required to define optional field
type.union<T>(...args: T[])
- example:
type.union(type.string, type.null)
- example:
#type ValueType<T>
Returns value object data type of the type parameter. Please use like following.
type PersonData = ValueType<Person>; // or `ValueType<typeof Person>;`
Credits
- This library is inspired by the prior arts below:
- The type definition system in this library is heavily influenced by io-ts.