Skip to content

Commit

Permalink
feat(TinyType): Single-line TinyType definitions with TinyTypeOf<T>
Browse files Browse the repository at this point in the history
  • Loading branch information
jan-molak committed Feb 12, 2018
1 parent 23ccbc7 commit e7f14c8
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 41 deletions.
130 changes: 129 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
[![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.
>
> &dash; [Jeff Bay, Object Calisthenics](http://www.xpteam.com/jeff/writings/objectcalisthenics.rtf)
### Single-value types

To define a single-value `TinyType` - extend from `TinyTypeOf<T>()`:

```typescript
import { TinyTypeOf } from 'tiny-types';

class FirstName extends TinyTypeOf<string>() {}
class LastName extends TinyTypeOf<string>() {}
class Age extends TinyTypeOf<number>() {}
```

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<string>() {}

class Timestamp extends TinyTypeOf<Date>() {}

abstract class DomainEvent extends TinyTypeOf<Timestamp>() {}

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! &#9733;

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 &copy; 2018- [Jan Molak](https://janmolak.com)_
116 changes: 80 additions & 36 deletions spec/TinyType.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string>() {}

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<string>() {}
class LastName extends TinyTypeOf<string>() {}

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<string>() {}
class Timestamp extends TinyTypeOf<Date>() {}

abstract class DomainEvent extends TinyTypeOf<Timestamp>() {}

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 {
Expand Down Expand Up @@ -76,7 +142,6 @@ describe('TinyTypes', () => {
expect(p1.equals(p3)).to.be.false; // tslint:disable-line:no-unused-expression
});


given(
undefined,
null,
Expand All @@ -93,49 +158,28 @@ describe('TinyTypes', () => {
});

describe('::toString', () => {
class Area extends TinyTypeOf<string>() {}
class District extends TinyTypeOf<number>() {}
class Sector extends TinyTypeOf<number>() {}
class Unit extends TinyTypeOf<string>() {}

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();
Expand All @@ -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))');
});
});
});
11 changes: 10 additions & 1 deletion src/TinyType.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export function TinyTypeOf<T>(): { new(_: T): { value: T } & TinyType } {
return class extends TinyType {
constructor(public readonly value: T) {
super();
}
};
}

export abstract class TinyType {

equals(another: TinyType) {
Expand Down Expand Up @@ -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();
}
}
3 changes: 0 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
export type List<T> = T[];
// export type Constructor<T> = new (...args: any[]) => T;
// export type Abstract<T> = Function & { prototype: T }; // tslint:disable-line:ban-types
// export type ConstructorOrAbstract<T = {}> = Constructor<T> | Abstract<T>;
export type ConstructorOrAbstract<T = {}> = Function & { prototype: T }; // tslint:disable-line:ban-types

0 comments on commit e7f14c8

Please sign in to comment.