Skip to content

Commit

Permalink
used a different underlying data structure, t() -> of() + updated readme
Browse files Browse the repository at this point in the history
  • Loading branch information
twop committed Jun 10, 2018
1 parent 08c0c2a commit d13b6e3
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 56 deletions.
66 changes: 40 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ NOTE: uses features from typescript 2.8
### Create

```typescript
import { Union, t } from 'ts-union';
import { Union, of } from 'ts-union';

const PaymentMethod = Union({
Cash: t(),
Check: t<CheckNumber>(),
CreditCard: t<CardType, CardNumber>()
Cash: of<void>(), // or just of()
Check: of<CheckNumber>(),
CreditCard: of<CardType, CardNumber>()
});

type CheckNumber = number;
Expand All @@ -35,7 +35,7 @@ const cash = PaymentMethod.Cash();
const check = PaymentMethod.Check(15566909);
const card = PaymentMethod.CreditCard('Visa', '1111-566-...');

// or destructure for simplicity
// or destructure to remove `PaymentMethod.` prefix.
const { Cash, Check, CreditCard } = PaymentMethod;
const anotherCheck = Check(566541123);
```
Expand All @@ -47,24 +47,24 @@ const str = PaymentMethod.match(cash, {
Cash: () => 'cash',
Check: n => `check num: ${n.toString()}`,
CreditCard: (type, n) => `${type} ${n}`
}); // cash
});
```

Also supports deferred (curried) matching and default case.
Also supports deferred (curried) matching and `default` case.

```typescript
const toStr = PaymentMethod.match({
Cash: () => 'cash',
default: _v => 'not cash' // _v is the union obj
});

const str = toStr(card); //not cash
const str = toStr(card); // "not cash"
```

### if (aka simplified match)

```typescript
const str = PaymentMethod.if.Cash(cash, () => 'cash'); //cash
const str = PaymentMethod.if.Cash(cash, () => 'cash'); // "cash"
// typeof str === string | undefined
```

Expand All @@ -81,15 +81,15 @@ const str = PaymentMethod.if.Check(

### Type of resulted objects

At the moment types of cash, check, card are opaque.
At the moment types of union values are opaque. That allows me to experiment with different underlying data structures.

```typescript
type CashType = typeof cash;
// OpaqueUnion<{Cash:..., Check:..., CreditCard:...}>
// and it is the same for card and check
```

The OpaqueUnion<...> type for PaymentMethod is accessible via T phantom property
The `OpaqueUnion<...>` type for `PaymentMethod` is accessible via phantom property `T`

```typescript
type PaymentMethodType = typeof PaymentMethod.T;
Expand All @@ -98,51 +98,50 @@ type PaymentMethodType = typeof PaymentMethod.T;

## Api and implementation details

If you will try to log the value for check you will see an array.
If you will try to log a union value you will see just an array.

```typescript
console.log(PaymentMethod.Check(15566909));
// ['Check', 15566909]
// ['Check', [15566909]]
```

All values are arrays. The first element is the key to match against and the rest is payload. I decided not to expose that through typings but I might reconsider that in the future. Although you cannot use it for redux action you can **safely use it for redux state**.
All union values are arrays. The first element is the key to match and the second is payload. I decided not to expose that through typings but I might reconsider that in the future. Although you cannot use it for redux action you can **safely use it for redux state**.

### Api

How to define shape

```typescript
const U = Union({
Simple: t(), // no payload
One: t<string>(), // one argument
Const: t(3), // one constant argument that is baked in
Two: t<string, number>(), // two arguments
Three: t<string, number, boolean>() // three
Simple: of(), // or of<void>(). no payload.
One: of<string>(), // one argument
Const: of(3), // one constant argument that is baked in
Two: of<string, number>(), // two arguments
Three: of<string, number, boolean>() // three
});
```

Let's take a closer look at `t` function
Let's take a closer look at `of` function

```typescript
export declare type Types = {
(): NoData;
<T>(): One<T>;
<T = void>(): T extends void ? NoData : One<T>;
<T>(val: T): Const<T>;
<T1, T2>(): Two<T1, T2>;
<T1, T2, T3>(): Three<T1, T2, T3>;
};
export declare const t: Types;
export declare const of: Types;
```

the actual implementation is pretty simple:

```typescript
export const t: Types = ((val: any) => val) as any;
export const of: Types = ((val: any) => val) as any;
```

We just capture the constant and don't really care about the rest. Typescript will guide us to provide proper number of args for each case.

match accepts either a full set of props or a subset with default case.
`match` accepts either a full set of props or a subset with a default case.

```typescript
// typedef for match function. Note there is a curried version
Expand All @@ -154,7 +153,7 @@ export type MatchFunc<Record> = {
};
```

if either accepts a function that will be invoked (with a match) and/or else case.
`if` either accepts a function that will be invoked (with a match) and/or else case.

```typescript
// typedef for if case for one argument.
Expand All @@ -164,3 +163,18 @@ if either accepts a function that will be invoked (with a match) and/or else cas
<R>(val: OpaqueUnion<Rec>, f: (a: A) => R, els: (v: OpaqueUnion<Rec>) => R): R;
}
```
And that is the whole api.
### Breaking changes from 1.1
* `t` function to define shapes is renamed to 'of'.
* There is a different underlying data structure. So if you persisted the values somewhere it won't be compatible with the new version.
The actual change is pretty simple:
```typescript
type OldShape = [string, ...payload[any]];
type NewShape = [string, payload[any]]; // ex
const example = ["Check", [15654747]]; // Note: nested array
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-union",
"version": "1.1.0",
"version": "1.2.0",
"description": "ADT sum types in ts that look similar to swift enums",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",
Expand Down
12 changes: 6 additions & 6 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// tslint:disable:no-expression-statement
import { test } from 'ava';
import { Union, t } from './index';
import { of, Union } from './index';

const U = Union({
Simple: t(),
One: t<string>(),
Const: t(3),
Two: t<string, number>(),
Three: t<string, number, boolean>()
Simple: of<void>(),
One: of<string>(),
Const: of(3),
Two: of<string, number>(),
Three: of<string, number, boolean>()
});

const { Simple, One, Two, Three, Const } = U;
Expand Down
44 changes: 23 additions & 21 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,24 @@ export type Case<T1, T2, T3> =
| Two<T1, T2>
| Three<T1, T2, T3>;

export type RecordDict = {
[key: string]: Case<any, any, any>;
};
export interface RecordDict {
readonly [key: string]: Case<any, any, any>;
}

export type ForbidReservedProps = {
if?: never;
match?: never;
T?: never;
readonly if?: never;
readonly match?: never;
readonly T?: never;
} & ForbidDefault;

export type Types = {
(): NoData;
<T>(): One<T>;
export interface Types {
<T = void>(): T extends void ? NoData : One<T>;
<T>(val: T): Const<T>;
<T1, T2>(): Two<T1, T2>;
<T1, T2, T3>(): Three<T1, T2, T3>;
};
}

export const t: Types = ((val: any) => val) as any;
export const of: Types = ((val: any) => val) as any;

export interface OpaqueUnion<Record> {
readonly _opaqueToken: Record;
Expand All @@ -53,9 +52,9 @@ export type Cases<Record, Result> = {
[T in keyof Record]: MatchCaseFunc<Record[T], Result>
};

export type ForbidDefault = {
export interface ForbidDefault {
readonly default?: never;
};
}

export type MatchCases<Record, Result> =
| Cases<Record, Result> & ForbidDefault
Expand Down Expand Up @@ -87,12 +86,12 @@ export type MatchCaseFunc<T, R> = T extends NoData
? (a1: A1, a2: A2, a3: A3) => R
: never;

export type MatchFunc<Record> = {
export interface MatchFunc<Record> {
<Result>(cases: MatchCases<Record, Result>): (
val: OpaqueUnion<Record>
) => Result;
<Result>(val: OpaqueUnion<Record>, cases: MatchCases<Record, Result>): Result;
};
}

export type UnpackFunc<T, Rec> = T extends NoData
? {
Expand Down Expand Up @@ -156,6 +155,7 @@ export type UnionObj<Record> = Constructors<Record> & {
export function Union<Record extends RecordDict & ForbidReservedProps>(
record: Record
): UnionObj<Record> {
// tslint:disable-next-line:prefer-object-spread
return (Object.assign(
{ if: createUnpack(record), match: createMatch() },
createContructors(record)
Expand All @@ -166,6 +166,7 @@ function createContructors<Record extends RecordDict>(
rec: Record
): Constructors<Record> {
const result: Partial<Constructors<Record>> = {};
// tslint:disable-next-line:forin
for (const key in rec) {
result[key] = createCtor(key, rec);
}
Expand All @@ -177,16 +178,18 @@ function createCtor<K extends keyof Record, Record extends RecordDict>(
rec: Record
): CreatorFunc<Record[K], OpaqueUnion<Record>> {
const val: Case<any, any, any> = rec[key];
// tslint:disable-next-line:no-if-statement
if (val !== undefined) {
const res = [key, val];
const res: ReadonlyArray<any> = [key, [val]];
return ((() => res) as any) as any;
}

return ((...args: any[]) => [key, ...args]) as any;
return ((...args: any[]) => [key, args]) as any;
}

function createUnpack<Record extends RecordDict>(rec: Record): Unpack<Record> {
const result: Partial<Unpack<Record>> = {};
// tslint:disable-next-line:forin
for (const key in rec) {
result[key] = createUnpackFunc(key, rec);
}
Expand All @@ -195,19 +198,18 @@ function createUnpack<Record extends RecordDict>(rec: Record): Unpack<Record> {

function createUnpackFunc<K extends keyof Record, Record extends RecordDict>(
key: K,
// tslint:disable-next-line:variable-name
_rec: Record
): UnpackFunc<Record[K], Record> {
return ((val: any, f: (...args: any[]) => any, els?: (v: any) => any) =>
val[0] === key ? f(...val.slice(1)) : els && els(val)) as any;
val[0] === key ? f(...val[1]) : els && els(val)) as any;
}

function createMatch<Record extends RecordDict>(): MatchFunc<Record> {
const evalMatch = (val: any, cases: MatchCases<Record, any>): any => {
// first elem is always the key
const handler = cases[val[0]] as any;
return handler
? handler(...val.slice(1))
: cases.default && cases.default(val);
return handler ? handler(...val[1]) : cases.default && cases.default(val);
};

return ((a: any, b?: any) =>
Expand Down
6 changes: 4 additions & 2 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"extends": ["tslint:latest", "tslint-config-prettier", "tslint-immutable"],
"extends": ["tslint:latest", "tslint-config-prettier"],
"rules": {
"interface-name": [true, "never-prefix"],
// TODO: allow devDependencies only in **/*.spec.ts files:
// waiting on https://github.com/palantir/tslint/pull/3708
"no-implicit-dependencies": [true, "dev"],

"object-literal-sort-keys": false,
/* tslint-immutable rules */
// Recommended built-in rules
"no-var-keyword": true,
Expand All @@ -28,7 +29,8 @@
true,
{ "ignore-prefix": ["console.", "process.exit"] }
],
"no-if-statement": true
"no-if-statement": true,
"no-shadowed-variable": false
/* end tslint-immutable rules */
}
}

0 comments on commit d13b6e3

Please sign in to comment.