From e7f14c87a642975f65c937fc3a00de78a75b90dc Mon Sep 17 00:00:00 2001 From: jan-molak Date: Mon, 12 Feb 2018 01:00:30 +0000 Subject: [PATCH] feat(TinyType): Single-line TinyType definitions with TinyTypeOf --- README.md | 130 +++++++++++++++++++++++++++++++++++++++++- spec/TinyType.spec.ts | 116 +++++++++++++++++++++++++------------ src/TinyType.ts | 11 +++- src/types.ts | 3 - 4 files changed, 219 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 62cc291b..c8474aa6 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,132 @@ [![Coverage Status](https://coveralls.io/repos/github/jan-molak/tiny-types/badge.svg?branch=master)](https://coveralls.io/github/jan-molak/tiny-types?branch=master) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![npm](https://img.shields.io/npm/dm/tiny-types.svg)](https://npm-stat.com/charts.html?package=tiny-types) -[![Known Vulnerabilities](https://snyk.io/test/github/jan-molak/tiny-types/badge.svg)](https://snyk.io/test/github/jan-molak/tiny-types) \ No newline at end of file +[![Known Vulnerabilities](https://snyk.io/test/github/jan-molak/tiny-types/badge.svg)](https://snyk.io/test/github/jan-molak/tiny-types) + +TinyTypes is an [npm module](https://www.npmjs.com/package/tiny-types) that makes it easy for TypeScript and JavaScript +projects to give domain meaning to primitive types. It also helps to avoid all sorts of bugs +and makes your code easier to refactor. + +## Installation + +To install the module from npm: + +``` +npm install --save tiny-types +``` + +## Defining Tiny Types + +> An int on its own is just a scalar with no meaning. With an object, even a small one, you are giving both the compiler +and the programmer additional information about what the value is and why it is being used. +> +> ‐ [Jeff Bay, Object Calisthenics](http://www.xpteam.com/jeff/writings/objectcalisthenics.rtf) + +### Single-value types + +To define a single-value `TinyType` - extend from `TinyTypeOf()`: + +```typescript +import { TinyTypeOf } from 'tiny-types'; + +class FirstName extends TinyTypeOf() {} +class LastName extends TinyTypeOf() {} +class Age extends TinyTypeOf() {} +``` + +Every tiny type defined this way has +a [readonly property](https://www.typescriptlang.org/docs/handbook/classes.html#readonly-modifier) +`value` of type `T`, which you can use to access the wrapped primitive value. For example: + +```typescript +const firstName = new FirstName('Jan'); + +firstName.value === 'Jan'; +``` + +It also has an `equals` method, which you can use to compare tiny types by value: + +```typescript +const + name1 = new FirstName('Jan'), + name2 = new FirstName('Jan'); + +name1.equals(name2) === true; +``` + +An additional feature of tiny types is a built-in `toString()` method (which you can override if you want to, of course): + +```typescript +const name = new FirstName('Jan'); + +name.toString() === 'FirstName(value=Jan)'; +``` + +### Multi-value and complex types + +If the tiny type you want to model has more than one value, +or you want to perform additional operations in the constructor, +extend from `TinyType` directly: + +```typescript +import { TinyType } from 'tiny-types'; + +class Person extends TinyType { + constructor(public readonly firstName: FirstName, + public readonly lastName: LastName, + ) { + super(); + } +} + +``` + +You can also mix and match both of the above definition styles: + +```typescript +import { TinyType, TinyTypeOf } from 'tiny-types'; + +class UserName extends TinyTypeOf() {} + +class Timestamp extends TinyTypeOf() {} + +abstract class DomainEvent extends TinyTypeOf() {} + +class AccountCreated extends DomainEvent { + constructor(public readonly username: UserName, timestamp: Timestamp) { + super(timestamp); + } +} + +const event = new AccountCreated(new UserName('jan-molak'), new Timestamp(new Date())); +``` + +Even such complex types still have both the `equals` and `toString` methods: + +```typescript +const + now = new Date(2018, 2, 12, 0, 30), + event1 = new AccountCreated(new UserName('jan-molak'), new Timestamp(now)), + event2 = new AccountCreated(new UserName('jan-molak'), new Timestamp(now)); + +event1.equals(event2) === true; + +event1.toString() === 'AccountCreated(username=UserName(value=jan-molak), value=Timestamp(value=Mon Mar 12 2018 00:30:00 GMT+0000 (GMT)))' +``` + +## Your feedback matters! + +Do you find TinyTypes useful? Give it a star! ★ + +Found a bug? Need a feature? Raise [an issue](https://github.com/jan-molak/tiny-types/issues?state=open) +or submit a pull request. + +Have feedback? Let me know on twitter: [@JanMolak](https://twitter.com/JanMolak) + +## License + +TinyTypes library is licensed under the [Apache-2.0](LICENSE.md) license. + +---- + +_- Copyright © 2018- [Jan Molak](https://janmolak.com)_ diff --git a/spec/TinyType.spec.ts b/spec/TinyType.spec.ts index dc794011..b945b2d8 100644 --- a/spec/TinyType.spec.ts +++ b/spec/TinyType.spec.ts @@ -1,9 +1,75 @@ import 'mocha'; import { given } from 'mocha-testdata'; -import { TinyType } from '../src/TinyType'; +import { TinyType, TinyTypeOf } from '../src'; import { expect } from './expect'; -describe('TinyTypes', () => { +describe('TinyType', () => { + + describe('definition', () => { + + it('can be a one-liner for TinyTypes representing a single value', () => { + class FirstName extends TinyTypeOf() {} + + const firstName = new FirstName('Jan'); + + expect(firstName.value).to.equal('Jan'); + expect(firstName).to.be.instanceOf(FirstName); + expect(firstName).to.be.instanceOf(TinyType); + expect(firstName.constructor.name).to.equal('FirstName'); + expect(firstName.toString()).to.equal('FirstName(value=Jan)'); + }); + + it('needs to extend the TinyType for types with more than one value', () => { + class FirstName extends TinyTypeOf() {} + class LastName extends TinyTypeOf() {} + + class FullName extends TinyType { + constructor(public readonly firstName: FirstName, + public readonly lastName: LastName) { + super(); + } + } + + const fullName = new FullName(new FirstName('Jan'), new LastName('Molak')); + + expect(fullName.firstName.value).to.equal('Jan'); + expect(fullName.lastName.value).to.equal('Molak'); + expect(fullName).to.be.instanceOf(FullName); + expect(fullName).to.be.instanceOf(FullName); + expect(fullName.constructor.name).to.equal('FullName'); + expect(fullName.toString()).to.equal('FullName(firstName=FirstName(value=Jan), lastName=LastName(value=Molak))'); + }); + + it('can be mixed and matched', () => { + const now = new Date(2018, 2, 12, 0, 30); + + class UserName extends TinyTypeOf() {} + class Timestamp extends TinyTypeOf() {} + + abstract class DomainEvent extends TinyTypeOf() {} + + class AccountCreated extends DomainEvent { + constructor(public readonly username: UserName, timestamp: Timestamp) { + super(timestamp); + } + } + + const e = new AccountCreated(new UserName('jan-molak'), new Timestamp(now)); + + expect(e.toString()).to.equal( + 'AccountCreated(username=UserName(value=jan-molak), value=Timestamp(value=Mon Mar 12 2018 00:30:00 GMT+0000 (GMT)))', + ); + + const + n = new Date(2018, 2, 12, 0, 30), + event1 = new AccountCreated(new UserName('jan-molak'), new Timestamp(now)), + event2 = new AccountCreated(new UserName('jan-molak'), new Timestamp(now)); + + expect(event1.equals(event2)).to.be.true; + + console.log(event1.toString()); + }); + }); describe('::equals', () => { class Name extends TinyType { @@ -76,7 +142,6 @@ describe('TinyTypes', () => { expect(p1.equals(p3)).to.be.false; // tslint:disable-line:no-unused-expression }); - given( undefined, null, @@ -93,49 +158,28 @@ describe('TinyTypes', () => { }); describe('::toString', () => { + class Area extends TinyTypeOf() {} + class District extends TinyTypeOf() {} + class Sector extends TinyTypeOf() {} + class Unit extends TinyTypeOf() {} + class Postcode extends TinyType { - constructor( - public readonly area: Area, - public readonly district: District, - public readonly sector: Sector, - public readonly unit: Unit, + constructor(public readonly area: Area, + public readonly district: District, + public readonly sector: Sector, + public readonly unit: Unit, ) { super(); } } - class Area extends TinyType { - constructor(public readonly code: string) { - super(); - } - } - - class District extends TinyType { - constructor(public readonly code: number) { - super(); - } - } - - class Sector extends TinyType { - constructor(public readonly code: number) { - super(); - } - } - - class Unit extends TinyType { - constructor(public readonly code: string) { - super(); - } - } - it('mentions the class and its properties', () => { const area = new Area('GU'); - expect(area.toString()).to.equal('Area(code=GU)'); + expect(area.toString()).to.equal('Area(value=GU)'); }); - - it('mentions the class and its fields', () => { + it('mentions the class and its fields, but not the methods', () => { class Person extends TinyType { constructor(public readonly name: string) { super(); @@ -158,7 +202,7 @@ describe('TinyTypes', () => { ); expect(postcode.toString()) - .to.equal('Postcode(area=Area(code=GU), district=District(code=15), sector=Sector(code=9), unit=Unit(code=NZ))'); + .to.equal('Postcode(area=Area(value=GU), district=District(value=15), sector=Sector(value=9), unit=Unit(value=NZ))'); }); }); }); diff --git a/src/TinyType.ts b/src/TinyType.ts index 51a84d18..f656b65d 100644 --- a/src/TinyType.ts +++ b/src/TinyType.ts @@ -1,3 +1,11 @@ +export function TinyTypeOf(): { new(_: T): { value: T } & TinyType } { + return class extends TinyType { + constructor(public readonly value: T) { + super(); + } + }; +} + export abstract class TinyType { equals(another: TinyType) { @@ -29,6 +37,7 @@ export abstract class TinyType { private fields() { return Object.getOwnPropertyNames(this) - .filter(field => typeof this[field] !== 'function'); + .filter(field => typeof this[field] !== 'function') + .sort(); } } diff --git a/src/types.ts b/src/types.ts index 0b241b7c..5c6d518f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,2 @@ export type List = T[]; -// export type Constructor = new (...args: any[]) => T; -// export type Abstract = Function & { prototype: T }; // tslint:disable-line:ban-types -// export type ConstructorOrAbstract = Constructor | Abstract; export type ConstructorOrAbstract = Function & { prototype: T }; // tslint:disable-line:ban-types