Ecosyste.ms: Awesome

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

Awesome Lists | Featured Topics | Projects

https://github.com/gcanti/babel-plugin-tcomb

Babel plugin for static and runtime type checking using Flow and tcomb
https://github.com/gcanti/babel-plugin-tcomb

Last synced: about 1 month ago
JSON representation

Babel plugin for static and runtime type checking using Flow and tcomb

Awesome Lists containing this project

README

        

Babel plugin for static and runtime type checking using Flow and tcomb.

**Tools**

[Flow](https://flowtype.org/) is a static type checker for JavaScript.

[tcomb](https://github.com/gcanti/tcomb) is a library for Node.js and the browser which allows you to check the types of JavaScript values at runtime with a simple and concise syntax. It's great for Domain Driven Design and for adding safety to your internal code.

# Why?

**Runtime type checking (tcomb)**

- you don't want or you can't use `Flow`
- you want refinement types
- you want to validate the IO boundary (for example API payloads)
- you want to enforce immutability
- you want to leverage the runtime type introspection provided by `tcomb`'s types

**Static type checking (Flow)**

`babel-plugin-tcomb` is `Flow` compatible, this means that you can run them side by side, statically checking your code with `Flow` and let `tcomb` catching the remaining bugs at runtime.

# Gentle migration path

You can add type safety to your untyped codebase gradually:

- first, add type annotations where you think they are most useful, file by file, leveraging the runtime type safety provided by `tcomb`
- then, when you feel comfortable, turn on `Flow` and unleash the power of static type checking
- third, for even more type safety, define your refinement types and validate the IO boundary

# Fork

[Here](https://github.com/christophehurpeau/babel-plugin-tcomb) you can find a fork of this plugin that provides the following additional features:

- Avoid checks on confident assignment
- Bounded polymorphism partial support
- `let` checks
- Assignment type checking

# Setup

First, install via npm.

```sh
npm install --save-dev tcomb
npm install --save-dev babel-plugin-tcomb
```

Then, in your babel configuration (usually in your `.babelrc` file), add (at least) the following plugins:

```js
{
"plugins" : [
"syntax-flow",
"tcomb",
"transform-flow-strip-types"
]
}
```

**Note**. ``syntax-flow`` and ``transform-flow-strip-types`` are already included with the [React Preset](https://babeljs.io/docs/plugins/preset-react/).

**Note**. Use [Babel's env option](https://babeljs.io/docs/usage/babelrc/) to only use this plugin in development.

**Warning**. If you use multiple presets and are experiencing issues, try tweaking the preset order and setting ``passPerPreset: true``. Related issues: [#78](https://github.com/gcanti/babel-plugin-tcomb/issues/78) [#99](https://github.com/gcanti/babel-plugin-tcomb/issues/99)

**Important**. `tcomb` must be `require`able

# Plugin configuration

## `skipAsserts?: boolean = false`

Removes the asserts and keeps the domain models

## `warnOnFailure?: boolean = false`

Warns (`console.warn`) about type mismatch instead of throwing an error

## `globals?: Array`

With this option you can handle global types, like `Class` or react `SyntheticEvent`

Example

```js
"plugins" : [
["tcomb", {
globals: [
// flow
{
'Class': true
}
// react
{
'SyntheticEvent': true,
...
},
// your custom global types (if any)
...
]
}],
]
```

# Definition files

Definition files for `tcomb` and `tcomb-react` are temporarily published [here](https://github.com/gcanti/pantarei).

# Caveats

- `tcomb` must be `require`able
- type parameters (aka generics) are not handled (`Flow`'s responsibility)

# How it works

First, add type annotations.

```js
// index.js

function sum(a: number, b: number) {
return a + b
}

sum(1, 'a') // <= typo
```

Then run `Flow` (static type checking):

```
index.js:7
7: sum(1, 'a')
^^^^^^^^^^^ function call
7: sum(1, 'a')
^^^ string. This type is incompatible with
3: function sum(a: number, b: number) {
^^^^^^ number
```

or refresh your browser and look at the console (runtime type checking):

```
Uncaught TypeError: [tcomb] Invalid value "a" supplied to b: Number
```

## Domain models

```js
// index.js

type Person = {
name: string, // required string
surname?: string, // optional string
age: number,
tags: Array
};

function getFullName(person: Person) {
return `${person.name} ${person.surname}`
}

getFullName({ surname: 'Canti' })
```

`Flow`:

```
index.js:14
14: getFullName({
^ function call
10: function getFullName(person: Person) {
^^^^^^ property `name`. Property not found in
14: getFullName({
^ object literal
```

`tcomb`:

```
TypeError: [tcomb] Invalid value undefined supplied to person: Person/name: String
```

## Refinements

In order to define [refinement types](https://github.com/gcanti/tcomb/blob/master/docs/API.md#the-refinement-combinator) you can use the `$Refinement` type, providing a predicate identifier:

```js
import type { $Refinement } from 'tcomb'

// define your predicate...
const isInteger = n => n % 1 === 0

// ...and pass it to the suitable intersection type
type Integer = number & $Refinement;

function foo(n: Integer) {
return n
}

foo(2) // flow ok, tcomb ok
foo(2.1) // flow ok, tcomb throws [tcomb] Invalid value 2.1 supplied to n: Integer
foo('a') // flow throws, tcomb throws
```

In order to enable this feature add the [`tcomb` definition file](https://github.com/gcanti/pantarei/blob/master/tcomb/3.x.x-0.33.x/tcomb.js) to the `[libs]` section of your `.flowconfig`.

## Runtime type introspection

Check out the [meta object](https://github.com/gcanti/tcomb/blob/master/docs/API.md#the-meta-object) in the tcomb documentation.

```js
import type { $Reify } from 'tcomb'

type Person = { name: string };

const ReifiedPerson = (({}: any): $Reify)
console.log(ReifiedPerson.meta) // => { kind: 'interface', props: ... }
```

In order to enable this feature add the [`tcomb` definition file](https://github.com/gcanti/pantarei/blob/master/tcomb/3.2.2%2B.js) to the `[libs]` section of your `.flowconfig`.

## Validating (at runtime) the IO boundary using typecasts

```js
type User = { name: string };

export function loadUser(userId: string): Promise {
return axios.get('...').then(p => (p: User)) // <= type cast
}
```

## Recursive types

Just add a `// recursive` comment on top:

```js
// recursive
type Path = {
node: Node,
parentPath: Path
};
```

# Type-checking Redux

```js
import { createStore } from 'redux'

// types
type State = number;
type ReduxInitAction = { type: '@@redux/INIT' };
type Action = ReduxInitAction
| { type: 'INCREMENT', delta: number }
| { type: 'DECREMENT', delta: number };

function reducer(state: State = 0, action: Action): State {
switch(action.type) {
case 'INCREMENT' :
return state + action.delta
case 'DECREMENT' :
return state - action.delta
}
return state
}

type Store = {
dispatch: (action: Action) => any;
};

const store: Store = createStore(reducer)

store.dispatch({ type: 'INCREMEN', delta: 1 }) // <= typo

// throws [tcomb] Invalid value { "type": "INCREMEN", "delta": 1 } supplied to action: Action
// Flow throws as well
```

# Type-checking React using tcomb-react

See [tcomb-react](https://github.com/gcanti/tcomb-react):

```js
// @flow

import React from 'react'
import ReactDOM from 'react-dom'
import { props } from 'tcomb-react'

type Props = {
name: string
};

@props(Props)
class Hello extends React.Component {
render() {
return

Hello {this.props.name}

}
}

ReactDOM.render(, document.getElementById('app'))
```

`Flow` will throw:

```
index.js:12
12: class Hello extends React.Component {
^^^^^ property `name`. Property not found in
19: ReactDOM.render(, document.getElementById('app'))
^^^^^^^^^ props of React element `Hello`
```

while `tcomb-react` will warn:

```
Warning: Failed propType: [tcomb] Invalid prop "name" supplied to Hello, should be a String.

Detected errors (1):

1. Invalid value undefined supplied to String
```

Additional babel configuration:

```js
{
"presets": ["react", "es2015"],
"passPerPreset": true,
"plugins" : [
"tcomb",
"transform-decorators-legacy"
]
}
```

In order to enable this feature add the [`tcomb-react` definition file](https://github.com/gcanti/pantarei/blob/master/tcomb-react/0.9.1%2B.js) to the `[libs]` section of your `.flowconfig`.
Also you may want to set `esproposal.decorators=ignore` in the `[options]` section of your `.flowconfig`.

### Without decorators

```js
// @flow

import React from 'react'
import ReactDOM from 'react-dom'
import { propTypes } from 'tcomb-react'
import type { $Reify } from 'tcomb'

type Props = {
name: string
};

class Hello extends React.Component {
render() {
return

Hello {this.props.name}

}
}

Hello.propTypes = propTypes((({}: any): $Reify))

ReactDOM.render(, document.getElementById('app'))
```

# Under the hood

## Primitives

```js
type MyString = string;
type MyNumber = number;
type MyBoolean = boolean;
type MyVoid = void;
type MyNull = null;
```

compiles to

```js
import _t from "tcomb";

const MyString = _t.String;
const MyNumber = _t.Number;
const MyBoolean = _t.Boolean;
const MyVoid = _t.Nil;
const MyNull = _t.Nil;
```

## Consts

```js
const x: number = 1
```

compiles to

```js
const x = _assert(x, _t.Number, "x");
```

Note: `let`s are not supported.

## Functions

```js
function sum(a: number, b: number): number {
return a + b
}
```

compiles to

```js
import _t from "tcomb";

function sum(a, b) {
_assert(a, _t.Number, "a");
_assert(b, _t.Number, "b");

const ret = function (a, b) {
return a + b;
}.call(this, a, b);

_assert(ret, _t.Number, "return value");
return ret;
}
```

where `_assert` is an helper function injected by `babel-plugin-tcomb`.

## Type aliases

```js
type Person = {
name: string,
surname: ?string,
age: number,
tags: Array
};
```

compiles to

```js
import _t from "tcomb";

const Person = _t.interface({
name: _t.String,
surname: _t.maybe(_t.String),
age: _t.Number,
tags: _t.list(_t.String)
}, "Person");
```