Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/oakfang/xype

Runtime JS type checking and matching
https://github.com/oakfang/xype

Last synced: 6 days ago
JSON representation

Runtime JS type checking and matching

Awesome Lists containing this project

README

        

# xype

Runtime JS type checking and matching

`xype` uses the new `Symbol.hasInstance` to create a robust solution for type-based code,
in the land of JavaScript.

## Install

`npm install --save xype`

## Usage

### Basic type-checking

```js
import { isinstance } from 'xype/type-utils';
import { number } from 'xype/primitives';
/*
primitives = {
number(1, 2.3, NaN, ...),
int(1, 2),
float(1, 1.3),
string('hello'),
bool(true, false),
nil(null, undefined),
fn(function foo() {})
}
isinstance = basically, instanceof as a function
*/

isinstance(3, number); // true
```

### Optional

`xype` introduces a new `optional` type, which functions much like haskell's `Maybe`.

```js
import { isinstance, number, optional } from 'xype';
const maybeNumber = optional(number);
isinstance(3, maybeNumber); // true
isinstance(null, maybeNumber); // true
isinstance('3', maybeNumber); // false
```

### Union

`xype` uses, and exposes, the `Union<...Ts>` type, which functions like the `type ZipCode = String | Number` declaration.

```js
import { isinstance, int, string, union } from 'xype';
const ZipCode = union(string, int);
isinstance(3, ZipCode); // true
isinstance(null, ZipCode); // false
isinstance('3', ZipCode); // true
```

### Records

```js
import { isinstance, string, number, nil, optional, record, union } from 'xype';
const Person = record({
name: string,
age: optional(number),
});

// Records can be extended infinitely
const AgelessPerson = Person.extended({ age: nil });
const p = { name: 'Foo' };
isinstance(p, Person); // true
isinstance(p, AgelessPerson); // true
p.age = 4;
isinstance(p, Person); // true
isinstance(p, AgelessPerson); // false
delete p.name;
isinstance(p, Person); // false
isinstance(p, AgelessPerson); // false

// Records are recursive
const Address = record({
city: string,
street: string,
house: int,
zip: optional(union(string, int)),
});
```

### Typed Arrays

```js
import { int } from 'xype/primitives';
import { arrayOf } from 'xype/compound';
import { isinstance } = require('xype/type-utils');

isinstance([2, 3, 4], arrayOf(int)); // true
isinstance([2, 3, '3'], arrayOf(int)); // false
isinstance([2, 3, '3'], arrayOf()); // true - defaults to type `any`
```

### Tuples

```js
import { int, string } from 'xype/primitives';
import { tuple } from 'xype/compound';
import { isinstance } = require('xype/type-utils');

const HTTPStatusCode = tuple(int, string);
isinstance([500, 'Internal Error'], HTTPStatusCode); // true
isinstance([500], HTTPStatusCode); // false
isinstance([500, 'Internal Error', 'Some Extra data'], HTTPStatusCode); // false
```

### Compound Reflection

```js
import { record } from 'xype/compound';
import { int, string } from 'xype/primitives';
import { optional, union } from 'xype/meta';
const EnabledUser = record({
username: string,
address: {
city: string,
street: string,
house: int,
zip: optional(union(int, string)),
},
enabled: true,
comments: [{ content: string }],
});
/*
This is equal to:
import { record, arrayOf } from 'xype/compound';
import { int, string } from 'xype/primitives';
import { optional, union, literal } from 'xype/meta';
const EnabledUser = record({
username: string,
address: record({
city: string,
street: string,
house: int,
zip: optional(union(int, string)),
}),
enabled: literal(true),
comments: arrayOf({ content: string })
});
*/
```

### Creating new types

```js
import { isinstance, typeby } from 'xype';
const EmptyArray = typeby(
instance => Array.isArray(instance) && instance.length === 0
);

isinstance([], EmptyArray); // true
isinstance([1], EmptyArray); // false

// however, for this instance you should probably go for a `tuple()`
```

## Matching

`xype` exposes a relatively powerful matching function, aimimng to emulate haskell's pattern-matching capabilities.

The `match` function matches a type/primitive and maps it to a function/value.
It uses the above mentioned reflection system for its `type` arguments, so objects are turned into `record` types,
arrays turn into `arrayOf` and numbers/strings/null/booleans become `literal` types.
Otherwise, see examples below:

```js
import { isinstance, primitives, optional, record, match, matchTo } from 'xype';
const isEven = match(
{
[int]: x => !(x % 2),
},
false
);
t.is(isEven(1), false);
t.is(isEven(2), true);
t.is(isEven('2'), false);

const getAge = match({ [AgelessPerson]: '-', [Person]: ({ age }) => age });

const p = { name: 'foo', age: 3 };
t.is(getAge(p), 3);
delete p.age;
t.is(getAge(p), '-');

const factorial = match({
[1]: 1,
[int]: n => n * factorial(n - 1),
});

t.is(factorial(3), 6);

const EmptyArray = typeby(arr => Array.isArray(arr) && arr.length === 0);

const tail = match(
{
[EmptyArray]: [],
[Array]: arr => arr.slice(1),
},
[]
);
t.is(tail([1, 2])[0], 2);
t.is(tail(0).length, 0);

const strictFib = match({
0: 1,
1: 1,
[int](x) {
return strictFib(x - 1) + strictFib(x - 2);
},
});
t.is(strictFib(4), 5);
t.throws(() => strictFib('foo'));

const get = match((prop, fallback = null) => ({
[record({ [prop]: not(nil) })]: obj => obj[prop],
[any]: fallback,
}));
const user = {
username: 'foobar',
};
t.is(get(user, 'username'), 'foobar');
t.is(get(user, 'meow', 5), 5);

// matchTo is used to match expression without creating a function
const num = 3;
const isNumEven = matchTo(
num, // notice the explicit parameter
{
[int]: x => !(x % 2),
},
false
); // false
```