Git Product home page Git Product logo

babel-plugin-tcomb's Introduction

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

Tools

Flow is a static type checker for JavaScript.

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 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.

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:

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

Note. syntax-flow and transform-flow-strip-types are already included with the React Preset.

Note. Use Babel's env option 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 #99

Important. tcomb must be requireable

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<Object>

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

Example

"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.

Caveats

  • tcomb must be requireable
  • type parameters (aka generics) are not handled (Flow's responsibility)

How it works

First, add type annotations.

// 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

// index.js

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

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 you can use the $Refinement type, providing a predicate identifier:

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<typeof isInteger>;

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 to the [libs] section of your .flowconfig.

Runtime type introspection

Check out the meta object in the tcomb documentation.

import type { $Reify } from 'tcomb'

type Person = { name: string };

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

In order to enable this feature add the tcomb definition file to the [libs] section of your .flowconfig.

Validating (at runtime) the IO boundary using typecasts

type User = { name: string };

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

Recursive types

Just add a // recursive comment on top:

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

Type-checking Redux

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:

// @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<void, Props, void> {
  render() {
    return <div>Hello {this.props.name}</div>
  }
}


ReactDOM.render(<Hello />, document.getElementById('app'))

Flow will throw:

index.js:12
 12: class Hello extends React.Component<void, Props, void> {
                                               ^^^^^ property `name`. Property not found in
 19: ReactDOM.render(<Hello />, 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:

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

In order to enable this feature add the tcomb-react definition file 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

// @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<void, Props, void> {
  render() {
    return <div>Hello {this.props.name}</div>
  }
}

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

ReactDOM.render(<Hello />, document.getElementById('app'))

Under the hood

Primitives

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

compiles to

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

const x: number = 1

compiles to

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

Note: lets are not supported.

Functions

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

compiles to

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

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

compiles to

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");

babel-plugin-tcomb's People

Contributors

0xflotus avatar amilajack avatar chrisblossom avatar christophehurpeau avatar ctrlplusb avatar gcanti avatar jeantimex avatar minedeljkovic avatar strml avatar voldern avatar xanf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

babel-plugin-tcomb's Issues

ReferenceError: Props is not defined

I took the code example from README and applied to my React component with tcomb-react. This is the error I'm getting:

ReferenceError: Props is not defined

Here is my code:

import { props } from 'tcomb-react'

type Props = {
  resourceName: ResourceNameT,
  resourceId: string
}

@props(Props)
export default class UserDefinedField extends React.Component {
...
}

Btw, I'm using the master branch of tcomb-react as well.

Cannot read property 'replace' of null

I get an error like this when I include this in my project:

Cannot read property 'replace' of null
    at Buffer.push (/Users/newuser/w/folio/node_modules/babel-core/lib/generation/buffer.js:269:16)
    at CodeGenerator.(anonymous function) [as push] (/Users/newuser/w/folio/node_modules/babel-core/lib/generation/index.js:525:15)

from this code:

create: ({node: n}): t.Object => {

Better error messages

Now:

import t from 'tcomb'

const Person = t.struct({
  name: t.String,
  surname: t.String
}, 'Person')

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

compiles to:

function getFullName(person: Person) {
  t.assert(Person.is(person));
  return `${person.name} ${person.surname}`
}

and you get the following message:

getFullName({name: 'Giulio', surname: 'Canti'}); // => throws "[tcomb] Assert failed (turn on "Pause on exceptions" in your Source panel)"

While it's useful (you can always inspect the debugger) it could be better.

My proposal would to compile to something like:

function getFullName(person) {
  t.assert(Person.is(person), function () { Person(person); return 'Invalid argument person (expected a ' + t.getTypeName(Person) + ')'})
  return `${person.name} ${person.surname}`
}

and then you get:

getFullName(); // => throws "[tcomb] Invalid value undefined supplied to Person (expected an object)"
getFullName({name: 'Giulio', surname: 'Canti'}); // => throws "[tcomb] Invalid argument person (expected a Person)"

/cc @ctrlplusb

Proposal: do not throw if a default import of tcomb is not available

This seems legit:

import {
  Number
} from 'tcomb';

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

and could be compiled to

import { Number } from 'tcomb';

function sum(a: Number, b: Number): Number {
  require("tcomb").assert(Number.is(a), 'Invalid argument a (expected a ' + require("tcomb").getTypeName(Number) + ')');
  require("tcomb").assert(Number.is(b), 'Invalid argument b (expected a ' + require("tcomb").getTypeName(Number) + ')');

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

  require("tcomb").assert(Number.is(ret), 'Invalid argument ret (expected a ' + require("tcomb").getTypeName(Number) + ')');
  return ret;
}

I propose to replace

function guardTcombImport() {
  if (!tcombLocalName) {
    throw new Error(
      'When setting type annotations on a function, an import of tcomb must be available within the scope of the function.');
  }
}

with (or something equivalent)

function guardTcombImport() {
  if (!tcombLocalName) {
    tcombLocalName = 'require("tcomb")';
  }
}

/cc @ctrlplusb

Assert on structure not specific instance type?

Hi, not sure if this is expected behaviour.

Given the following:

const Person = t.struct({
  name: t.String,
  surname: t.String
}, `Person`);

function hello(x : Person) {
  return 'Hello ' + x.name;
}

Executing the following errors:

hello({ name: `jon`, surname: `doe`});

Whereas the following does not:

hello(Person({ name: `jon`, surname: `doe`}));

This seems a bit overly strict in my opinion. I know it transpiles to:

t.Assert(Person.is(x));

Do you think it's worth it and possible to relax the rules so that we assert the structure only?

Sometimes I just want to wrap a complex argument object into a local struct and not necessarily expose the type.

Consider supporting an alternative format for default param type annotations.

Currently the following syntax is supported:

function foo(bar = 1 : t.Number) 

Would you be up for us to support the following additional syntax:

function foo(bar : t.Number  = 1) 

It seems like Flow annotations seemed to have followed this additional style. Therefore tools like EsLint don't complain for this format, however, they throw errors on the current syntax.

Definition file for tcomb

The new version (v0.3) of this plugin will require a definition file in order to exploit refinements and runtime type introspection.

I'm working on a first draft but I'm not sure what's the best practice. Where can I put the definition file?

  • tcomb's' repo
  • this repo
  • a new dedicated repo

Using with Karma + Webpack

I'm having a hard time running this plugin as part of my Karma test run. I'm using webpack as a preprocessor. I figured the types would be stripped by Webpack's Babel loader, but that is not happening. This is the error I'm getting when I run the tests:

Module build failed: TypeError: ../autocomplete/index.js: Cannot read property 'replace' of null
  at Buffer.push (../node_modules/babel-core/lib/generation/buffer.js:273:16)

I tried both with and without the plugin configured as a loader in my webpack configuration file:

loaders: ['babel?plugins[]=rewire,plugins[]=tcomb']

But that doesn't seem work. I'm at a loss.

How to enforce a fixed arity in a function

Flow treats every function as variadic:

function foo(x: string) {}

foo('a', 1) // <= this is ok for Flow

A (hackish?) possible solution:

type Empty = void & null; // <= represents the empty set

function foo(x: string, ...rest: Array<Empty>) {}

foo('a', 1) // Flow complains

throws

15: foo('a', 1)
     ^^^^^^^^^^^^^^^^^ function call
 15: foo('a', 1)
                    ^ number. This type is incompatible with
 11: declare type Empty = void & null;
                                 ^^^^ null

flow-compatible abstraction over tcomb

Can you explain in readme, why you abandon idea of unobtrusive flow-based type-checking? (https://github.com/gcanti/flowcheck)

babel-plugin-tcomb doesn't support flow type annotations. It support you own standard of type declarations, based on tcomb. It's so difficult to create gracefully degraded to flow abstraction over tcomb?

add support for default values

This example throws an error with the current implementation

function foo(x: t.Number = 0, y: t.String) {
  return x + y;
}

support values in type casts

Currently only identifiers are supported, this throws:

const a = ('a value': A)
TypeError: Property value expected type of string but got null

Situations with metadata, when flow definition placed in external library

By #11

Problem:

Library declaration:

// node_modules/myLib/flow-typed/interface.js
declare module 'myLib' {
  declare interface User {
    name: string;
  }
}

In application we can't access type metadata:

// app.js
import type {User} from 'myLib'

function (user: User) {
// ...
}

Type system is separate from javascript. Flow or typescript does not help, types as value keys not supported.

Only tricks, both with some restrictions:

Threat type name + import path as unique string key

myLib interface declaration:

// @flow
// node_modules/myLib/flow-typed/interfaces.js
declare interface User {
  name: string;
}

In entry point of myLib add type reflection to singleton tcombRegistry object by this key.

// @flow
// node_modules/myLib/index.js

import type {User} from 'myLib'

import {$tcombGetMeta} from 'tcomb'
$tcombSetMeta('User.myLib', definition)

In application access tcombRegistry by generated key.

// @flow
// app.js
import type {User} from 'myLib'
import {$tcombGetMeta} from 'tcomb'
$tcombGetMeta('User.myLib')

Use export type

Declarations for interfaces in flowtype looks like workaround. No common way to declare interface for internal and external usage.

Internal interface:

// @flow
// myLib/src/i/pub.js
export type User = {
  name: string;
}

Before publishing, we can preprocess this interface with separate configuration, only with babel-plugin-tcomb. Preprocessed file placed to myLib/i, instead of myLib/src/i. Directory 'myLib/i' included to files section of package.json.

// @flow
// myLib/i/pub.js
export type User = {
  name: string;
}
export $tcombUserMeta = {}

In application:

// @flow
// app.js
import type {User} from 'myLib/i/pub'
import  {$tcombUserMeta} from 'myLib/i/pub'

Global available types like SyntheticEvent are not found

I get this error: SyntheticEvent is not defined when using the type SyntheticEvent in my code.

For some reason flow seems to understand what the type means and babel-plugin-tcomb does not.

I have tried to import the type but that upsets flow:

import type { SyntheticEvent } from 'react'; gives: This module has no named export called SyntheticEvent.

Flow defines this type here: https://github.com/facebook/flow/blob/master/lib/react.js it seems to globally declared, i.e. not tied to any specific module.

It looks like babel-plugin-tcomb does not understand globally declared types.

Adding type safety, gradually. Part II

In the previous post we covered how to get started with babel-plugin-tcomb and flow. In this post we want to introduce a unique feature of tcomb which will be especially interesting for flow users: refinement types.

Refinement types

A refinement type is a type endowed with a predicate which must hold for all instances of the refined type.

That might sound complicated, but really it's very straight forward. Here's an example using vanilla tcomb:

import t from 'tcomb';

const PositiveNumber = t.refinement(
  t.Number,       // <= the type we wish to refine.
  (n) => n >= 0   // <= the predicate that enforces our desired refinement.
);

PositiveNumber(1); // => ok
PositiveNumber(-2); // => throws [tcomb] Invalid value -2 supplied to {Number | <function1>}

There are no limits on what you can do within your predicate declarations. This means you can narrow your types by defining precise invariants, something that static type checkers can do only partially.

Refinements are a very powerful runtime type checking capability of tcomb, but how could we access this power when we are using flow?

Flow

flow can't enforce refinements since they require runtime execution, however when used in combination with babel-plugin-tcomb we can get flow to do our static type checking against the type we are refining whilst also declaring where we would like our runtime refinements to be enforced.

In order for you to define your refinements tcomb exposes the following flow interface:

declare interface $Refinement<P>: Predicate> {}

The $Refinement<P> interface accepts a type parameter P that must be a Predicate (see Bounded Polymorphism for more info). Remember, all predicates need to adhere to the following flow definition:

declare type Predicate = (x: any) => boolean;

Using the $Refinement<P> interface allows you to easily define refinement types. We will explain the usage of this interface via an example.

Let's say that you would like to create a refinement type to enforce that numbers be positive (much like the tcomb example above).

Firstly, you need to define a standalone predicate function that can be used to enforce this rule:

const isPositive = (n) => n >= 0;

A very simple function that takes a number and then ensures the number is greater than or equal to zero.

We can then use this predicate function to define our refinement type by making use of our special $Refinement<P> interface along with a type intersection against the type we are attempting to refine. In this case we are refining the number type.

Here is the complete example on how you would then declare your refinement type:

import type { $Refinement } from 'tcomb';

const PositiveNumber = 
    // The type that we are refining.
    number
    // The intersection operator.
    &
    // The refinement we would like to enforce.
    $Refinement<typeof isPositive>;

We can now use this refinement type as a standard flow type annotation, like so:

function foo(n: PositiveNumber) { }

There are some things you need to note here.

  • flow will do static analysis to ensure that the argument to our foo function is in fact a number.
  • babel-plugin-tcomb interprets our refinement type declaration and ensures that the argument to foo will be checked by our refinement function during runtime.

Let's see what the result would be for various executions of our foo function:

foo(2)    // static checking ok,     runtime checking ok
foo(-2)   // static checking ok,     runtime checking throws "[tcomb] Invalid value -2 supplied to n: PositiveNumber"
foo('a')  // static checking throws, runtime checking throws

Static and runtime type checking are both useful and they are completely complementary: you can get the best of both worlds!

It is also worth noting that your $Refinement<P> declarations are statically type-checked: so if you pass an invalid predicate to $Refinement<P> then Flow will complain:

const double = n => 2 * n; // Invalid! returns a number, not a boolean

type PositiveNumber =
  number &
  $Refinement<typeof double>;

const n: PositiveNumber = 1;

Output:

src/index.js:5
  5: const double = n => 2 * n;
                         ^^^^^ number. This type is incompatible with
  9:   declare interface $Refinement<P: (x: any) => boolean> {}
                                                    ^^^^^^^ boolean. See lib: definitions/tcomb.js:9

Hopefully this post helps to illustrate some of the power in having runtime enforced refinements. With this capability you are able to declare refinement types that could do things like ensure a string is actually a well formed UUID or URL - something that can be tedious to manually test throughout your codebase without having the power of refined types at your fingertips.

struct type in the argument throws an error

I've been trying to use a struct type in my argument in Node, but it keeps throwing this error.

TypeError: [tcomb] Assert failed (turn on "Pause on exceptions" in your Source panel)

Here is the code that triggered it.

const pipe_args_t = t.struct({query: t.Object});
...
(args: pipe_args_t) => {
     ....
}

add support for strictness?

Requirements

Example

type Person = {
  name: string
};

// this is ok for both flow and babel-plugin-tcomb
const p: Person = { name: 'Giulio', age: 42 }

but if we use $Strict should raise an error at runtime (as Flow does statically)

import type { $Strict } from 'tcomb' // <= type $Strict<T> = T & $Shape<T>;

type Person = $Strict<{
  name: string
}>;

// here should throw
const p: Person = { name: 'Giulio', age: 42 }

Flow error

src/index.js:9
  9: const p: Person = { name: 'Giulio', age: 42 }
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^ property `age` of object literal. Property not found in
  5: type Person = $Strict<{
                           ^ object type

Implementation

type Person = $Strict<{
  name: string
}>;

should compile to something like

var Person = _t.interface({
  name: _t.String
}, { name: 'Person', strict: true });

Flow vs PureScript, aka how to write some PureScript idioms with JavaScript + type annotations

Note. The term "FlowScript" here means JavaScript + Flow type annotations (+ PureScript idioms).

Functions

A basic example

PureScript

add :: Int -> Int -> Int
add x y = x + y

add 10 20

In FlowScript all functions must be curried

const add: (_: number) => (_: number) => number =
  x => y => x + y

add(10)(20)

Let's introduce some helper types in order to avoid such a boilerplate

// FunctionN, where N = function arity
type Function1<A, B> = (_: A) => B;
type Function2<A, B, C> = (_: A) => (_: B) => C;
type Function3<A, B, C, D> = (_: A) => (_: B) => (_: C) => D;

Now add is more readable:

const add: Function2<number, number, number> =
  x => y => x + y

A function with universally quantified type

PureScript

flip :: forall a b c. (a -> b -> c) -> b -> a -> c
flip f y x = f x y

In FlowScript, as a convention, every type parameter is uppercase

export function flip<A, B, C>(f: Function2<A, B, C>): Function2<B, A, C> {
  return y => x => f(x)(y)
}

const f: Function2<number, string, number> = n => s => s.length + n

let..in

PureScript

example :: Number -> Number -> Number -> Number
example x y z =
  let foo = x * y in
  let bar = y * z in
  foo + bar

in FlowScript lets are translated to consts

export const example: Function3<number, number, number, number> =
x => y => z => {
  const foo = x * y
  const bar = y * z
  return foo + bar
}

Data structures

type

PureScript

type Address =
  { street :: String
  , city   :: String
  , state  :: String
  }

type Entry =
  { firstName :: String
  , lastName  :: String
  , address   :: Address
  }

It's the same in FlowScript

type Address = {
  street: string,
  city: string,
  state: string
};

type Entry = {
  firstName: string,
  lastName: string,
  address: Address
};

data

data Maybe a = Nothing | Just a

x :: Maybe Bool
x = Just false

y :: Maybe Int
y = Just 1

FlowScript

export type Maybe<A> = { type: 'Nothing' } | { type: 'Just', value: A };

const x: Maybe<boolean> = { type: 'Just', value: false } // boilerplate

const y: Maybe<number> = { type: 'Just', value: 1 } // boilerplate

Again, let's introduce some helpers

// Maybe helpers, aka type constructors
export function Nothing(): Maybe<*> {
  return { type: 'Nothing' }
}

export function Just<A>(value: A): Maybe<A> {
  return { type: 'Just', value }
}

or even better

export function Nothing(): Maybe<*> {
  return Nothing.value
}

Nothing.value = { type: 'Nothing' }

export function Just<A>(value: A): Maybe<A> {
  return { type: 'Just', value }
}

Now building some Maybes is more handy:

const x: Maybe<boolean> = Just(false)

const y: Maybe<number> = Just(1)

Another example.

PureScript

type Point = { x :: Number, y :: Number }

data Shape
  = Circle Point Number
  | Rectangle Point Number Number
  | Line Point Point
  | Text Point String

FlowScript

export type Point = {
  x: number,
  y: number
};

export type Shape
  = { type: 'Circle', center: Point, radius: number }
  | { type: 'Rectangle', position: Point, height: number, width: number }
  | { type: 'Line', start: Point, end: Point }
  | { type: 'Text', position: Point, label: string };

newtype

No equivalent :(

Recursive data structures

PureScript

data List a = Nil | Cons a (List a)

FlowScript

type List<A> = { type: 'Nil' } | { type: 'Cons', head: A, tail: List<A> };

Type classes

PureScript

class Show a where
  show :: a -> String

FlowScript

export type Show<A> = {
  show: Function1<A, string>;
};

export function show<A>(dictShow: Show<A>): Function1<A, string> {
  return dictShow.show
}

Instances

PureScript

instance showBoolean :: Show Boolean where
  show true = "true"
  show false = "false"

instance showMaybe :: (Show a) => Show (Maybe a) where
  show Nothing = "Nothing"
  show (Just x) = "Just(" <> show x <> ")"

FlowScript

export const showBoolean: Show<boolean> = {
  show(x) {
    return x ? 'true' : 'false'
  }
}

export function showMaybe<A>(dictShow: Show<A>): Show<Maybe<A>> {
  return {
    show(x) {
      switch (x.type) {
        case 'Nothing' :
          return 'Nothing'
        case 'Just' :
          return 'Just(' + show(dictShow)(x.value) + ')'
      }
      throw new Error("Failed pattern match")
    }
  }
}

note how showMaybe takes an additional dictShow argument because of the (Show a) => contraint.

Using show

PureScript

show ( Just false ) -- "Just(false)"

FlowScript (a bit verbose...)

show(showMaybe(showBoolean))(Just(false)) // "Just(false)"

Nice to have: eslint plugin to allow for "unused" t imports.

EsLint isn't intelligent enough to pick up the relationship with an import of t which isn't being used directly. e.g.

import t from 'tcomb';
import { Person } from './types';
function hello(person: Person) {
...
}  

This can cause build fails on strict setups.

You can create an .eslintrc rule as so to naively fix this:

    "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "varsIgnorePattern": "t" }],

That will ignore any unused vars with the identifier of t.

Of course this isn't bulletproof though, so an intelligent AST based eslint plugin that is a reverse of #21 could be useful.

This is strictly for the most pedantic of us.

Double assertion

Input:

register(token: string) {
    // ...
},

Output:

register: function register(token) {
    _assert(token, _tcomb2.default.String, 'token');

    _assert(token, _tcomb2.default.String, 'token');

    // ...
},

Support for Babel 6

I am sure this is fairly obvious and probably just needs someone (me?) to get on with it. I thought that it best to have it logged though.

Thanks for the great work on your libraries.

The Eff monad

Adapted from "Supercharged Types" (http://rtpg.co/2016/07/20/supercharged-types.html) by @rtpg

Flow has Row Polymorphism, an important feature if you want to encode invariants into types. For people with Haskell experience, Eff is kinda like IO, things with side effects end up in Eff (PureScript users should feel comfortable).

Here's a tiny example. I'm not going to explain too much about the type mechanisms themselves, just a taste of what is possible.

type DB = { type: 'DB' };

type User = {
  username: string,
  uid: number
};

function createUser(username: string): Eff<{ write: DB }, User> {
  ...
}

function lookupUser(username: string): Eff<{ read: DB }, ?User> {
  ...
}
  • the type DB represents a side effect
  • the type of createUser is (username: string) => Eff<{ write: DB }, User>. It means that createUser writes to the DB and gives you a User back
  • the type of lookupUser is similar: (username: string) => Eff<{ read: DB }, ?User>. Given a string (in our case a username), it will return an action that will read from the DB and return a User (if found).

And here's a function that will create a user and then look it up

const createThenLookupUser = username => createUser(username).chain(user => lookupUser(user.uid))

What's the type of createThenLookupUser? Let's ask Flow!

$> flow suggest index.js

const createThenLookupUser = username => createUser(username).chain(user: {uid: number, username: string} => lookupUser(user.uid): Eff<{read: {type: 'DB'}}, ?{uid: number, username: string}>): Eff<{write: {type: 'DB'}} & {read: {type: 'DB'}}, ?{uid: number, username: string}>

Flow is quite verbose but if you skip the cruft you can see

(username: string) => Eff<{ write: DB, read: DB }, ?User>

The type inference figured out that createThenLookupUser:

  • writes to the DB (write: DB)
  • reads from the DB (read: DB)

Row polymorphism here is used to encode effects. We can combine actions within Eff, and the type system will accumulate the effects and keep track of them.

Whitelisting side effects

Let's see how you can encode a whitelist of accepted effects in a function signature. I'm going to write a model for a (server side) router.

First some types

type Method = 'GET' | 'POST';

// web request
type Request = {
  body: string,
  header: string,
  method: Method
};

// web response
type Response = {
  body: string,
  status: number
};

Second, let's write an endpoint to register to our service

function signupPage(req: Request): Eff<{ write: DB, read: DB }, Response> {
  const username = req.header
  return lookupUser(username).chain(user => {
    if (user) {
      return new Eff(() => ({
        body : "A user with this username already exists!",
        status : 400
      }))
    }
    return createUser(username).map(() => ({
      body : "Created User with name " + username,
      status: 200
    }))
  })
}

Now let's write the router. A Route is either a GET or a POST endpoint. We want to enforce that GET endpoints can't write to the db

type GetRoute = {
  type: 'GetRoute',
  path: string,
  handler: (req: Request) => Eff<{ read: DB }, Response>
};

type PostRoute = {
  type: 'PostRoute',
  path: string,
  handler: (req: Request) => Eff<{ read: DB, write: DB }, Response>
};

type Route = GetRoute | PostRoute;

Finally our main routes

const routes: Array<Route> = [
  { type: 'GetRoute', path: '/signup', handler: signupPage }
]

But if you run Flow you get the following error

 73:   { type: 'GetRoute', path: '/signup', handler: signupPage }
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ object literal. This type is incompatible with
 72: const routes: Array<Route> = [
                         ^^^^^ union: GetRoute | PostRoute
  Member 1:
   70: type Route = GetRoute | PostRoute;
                    ^^^^^^^^ GetRoute
  Error:
   42: function signupPage(req: Request): Eff<{ write: DB, read: DB }, Response> {
                                              ^^^^^^^^^^^^^^^^^^^^^^^ property `write`. Property not found in
   61:   handler: (req: Request) => Eff<{ read: DB }, Response>
                                        ^^^^^^^^^^^^ object type

This is great! signupPage totally writes to the DB! And GETs should not be allowed to change the server state.

Changing the route definition to

const routes: Array<Route> = [
  { type: 'PostRoute', path: '/signup', handler: signupPage }
]

solves the problem: the endpoint is now allowed to write to the DB, because of the types of PostRoute and signupPage.

The Eff monad

export default class Eff<EA: Object, A> {
  run: () => A;
  constructor(run: () => A) {
    this.run = run
  }
  map<B>(f: (_: A) => B): Eff<EA, B> {
    return new Eff(() => f(this.run()))
  }
  chain<EB: Object, B>(f: (_: A) => Eff<EB, B>): Eff<EA & EB, B> {
    return Eff.join(this.map(f))
  }
  static join<EA: Object, EB: Object, B>(x: Eff<EB, Eff<EA, B>>): Eff<EA & EB, B> {
    return new Eff(() => x.run().run())
  }
  ...
}

Should we assert that an import of tcomb occurred?

Transpile output is:

const f = x => {
  t.assert(t.String.is(x));
  return x;
};

Therefore if t isn't in scope the user will get runtime errors. I can put a check within the plugin that will assert for a tcomb import.

What do you think?

How to type check enums in switch statements

This doesn't raise errors (seems a bug in Flow facebook/flow#1835):

type Action = { type: 'A' } | { type: 'B' };

export function foo(action: Action): number {
  switch (action.type) {
    case 'AA' : // <= typo
      return 1
    case 'B' :
      return 2
  }
  return 0
}

Can be alleviated by introducing an auxiliary enum and using a type cast in each case:

type ActionType = 'A' | 'B';
type Action = { type: 'A' } | { type: 'B' };

export function foo(action: Action): number {
  switch (action.type) {
    case ('AA': ActionType) :
      return 1
    case ('B': ActionType) :
      return 2
  }
  return 0
}

raises

src/index.js:8
  8:     case ('AA': ActionType) :
               ^^^^ string. This type is incompatible with
  8:     case ('AA': ActionType) :
                     ^^^^^^^^^^ string enum

Cons

  • duplicated values in type ActionType

Add support for Variable declarations (let)

Example

let bar: bool = foo;

bar = true;

compiles to

let bar = _assert(foo, _t.Boolean, "bar");

bar = _assert(true, _t.Boolean, "bar");

@christophehurpeau let me know if you want to work on this. From my part I'd love to help you out writing a test suite which I'll post later here or in a apposite branch

a few issues

Hi there, I'm trying to use babel-tcomb-plugin here: buildo/avenger#82 and it is working really well for now!

A few issues a run into:

ret function in non-binded class methods

here: https://github.com/buildo/avenger/blob/avenger-81-use_babelplugintcomb/src/Avenger.js#L73 I'd like to type the return value as PromiseType (instanceof Promise). The generated code looks something like:

Avenger.prototype.run = function run(input, state) {
  input = _types.AvengerInput(input);
  state = _types.State(state);

  var ret = function (input, state) {
    var _this2 = this;

    // ...

    return //...
  }(input, state);

  return _types.PromiseType(ret);
};

No matter what tcomb type annotation I use there, but I get many problems due to this being undefined inside ret = function(input, state) { ... }.
I could either bind the method myself in class declaration or handle with more care the invocation everywhere else in my code, but.. what if the generated code was something like the following instead?

Avenger.prototype.run = function run(input, state) {
  input = _types.AvengerInput(input);
  state = _types.State(state);

  var ret = (function (input, state) {
    var _this2 = this;

    // ...

    return //...
  }).call(this, input, state);

  return _types.PromiseType(ret);
};

types and default value for arrow functions params

Not sure about what you could do on your side here (didn't look at the code yet), but I wasn't able to apply both a type annotation and a default value here: https://github.com/buildo/avenger/blob/avenger-81-use_babelplugintcomb/src/util.js#L20 and in other similar cases.. any suggestion?

What I'm trying to do is: export const collect = (o: t.Object, map: t.Function = v => v) => ...

RFC: How to statically type check model updates

This is a follow up of this chat with @emirotin on gitter

Problem: t.update is not suited for static type checking

// @flow

import t from 'tcomb'

type Obj = {
  a: number
};

const obj1: Obj = { a: 1 }
const obj2: Obj = t.update(obj1, { a: { $set: 'a' } }) // <= typo

// result: no errors for Flow, tcomb throws at runtime

The gist is that I wrote the t.update API 2 years ago, when static type checking in JavaScript was not a thing, so it leverages the dynamic nature of JavaScript and is not suited for static type checking (I tried to write a definition file for it but it's almost impossible).

We need a different API, an API which Flow can understand and fully type check. For example Elm has a specific syntax for that (and PureScript as well). Supposedly such an API will require you to write more boilerplate but you get type safety in return.

I opened this issue to gather ideas and suggestions on this topic.

Add support for Variable declarations (const)

Hello !
Destructuring is not checked by tcomb.

const { a }: { a: string } = { a: 'a' };

transpiles to:

var _a = { a: 'a' };
const a = _a.a;

It works with babel-plugin-typecheck:

var _a = { a: 'a' };
const a = _a.a;

if (!(typeof a === 'string')) {
    throw new TypeError('Value of "{\n  a\n}" violates contract.\n\nExpected:\n{ a: string\n}\n\nGot:\n' + _inspect({ a: a }));
}

Support for "Object" type annotations.

function hello(person : { name: t.String, surname: t.String }) {
  return `Hello ${name} ${surname}`;
}

transpiled into something like

function hello(person : { name: t.String, surname: t.String }) {
  t.assert(t.String.is(person.name));
  t.assert(t.String.is(person.surname));
  return `Hello ${person.name} ${person.surname}`;
}

replace asserts by calling constructors to asserts by calling is() methods

Now the plugin does

const Person = t.struct({
  name: t.String
});

function foo(person: Person) {
  return person.name;
}

// compiles to
function foo(person: Person) {
  person = Person(person);

  return person.name;
}

Should compile to

function foo(person: Person) {
  t.assert(Person.is(person));

  return person.name;
}

Adding type safety, gradually. Part I

Goal

The goal of this series of posts is to show how you can add type safety, both statically and at runtime, to your untyped codebase gradually and with a gentle migration path.

Static and runtime type checking are complementary and you can get benefits from both.

Tools

I will use the following tools:

  • Runtime type checking
    • 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.
    • babel-plugin-tcomb is a babel plugin which compiles Flow type annotations to corresponding tcomb models and asserts.
  • Static type checking (optional)
    • Flow is a static type checker for JavaScript.

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 (e.g. 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

Setup

First, install via npm:

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

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

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

If you are using the react preset, the babel-plugin-syntax-flow and babel-plugin-transform-flow-strip-types plugins are already included:

{
  "presets": ["react", "es2015"],
  "passPerPreset": true, // <= important!
  "plugins" : [
    "tcomb"
  ]
}

You can download Flow from here.

Get started

Say you have this untyped function:

function sum(a, b) {
  return a + b;
}

Adding type annotations is easy, just add a colon and a type after each parameter:

// means "both `a` and `b` must be numbers"
function sum(a: number, b: number) {
  return a + b;
}

For a quick reference on type annotations, start here.

Type annotations are not valid JavaScript, but they will be stripped out by babel-plugin-transform-flow-strip-types so your code will run as before.

Now let's introduce intentionally a bug:

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

sum(1, 2);   // => ok
sum(1, 'a'); // => throws Uncaught TypeError: [tcomb] Invalid value "a" supplied to b: Number

screen shot 2016-06-23 at 11 09 50

Note that you can inspect the stack in order to find where the error was originated. The power of Chrome Dev Tools (or equivalent) are at your disposal.

Runnning Flow

In order to run Flow, just add a .flowconfig file to your project and a comment:

// @flow

at the beginning of the file. Then run flow from you command line. Here's the output:

$> flow
src/index.js:7
  7: sum(1, 'a'); // => throws Uncaught TypeError: [tcomb] Invalid value "a" supplied to b: Number
     ^^^^^^^^^^^ function call
  7: sum(1, 'a'); // => throws Uncaught TypeError: [tcomb] Invalid value "a" supplied to b: Number
            ^^^ string. This type is incompatible with
  2: function sum(a: number, b: number) {
                                ^^^^^^ number

Types

You are not limited to primitive types, this is a annotated function which works on every object that owns a name and a surname property:

function getFullName(x: { name: string, surname: string }) {
  return x.name + ' ' + x.surname;
}

getFullName({ name: 'Giulio' }); // => throws Uncaught TypeError: [tcomb] Invalid value undefined supplied to x: {name: String, surname: String}/surname: String

All the Flow type annotations are supported.

Immutability

Immutability is enforced by tcomb at runtime. The values passed "through" a type annotation will be immutables:

function getFullName(x: { name: string, surname: string }) {
  return x.name + ' ' + x.surname;
}

var person = { name: 'Giulio', surname: 'Canti' };
getFullName(person);
person.name = 1; // throws TypeError: Cannot assign to read only property 'name' of object '#<Object>'

Next post

In the next post I'll talk about how to tighten up your types with the help of refinements.

Note. If you are interested in the next posts, watch this repo, I'll open a new issue for each of them when they are ready.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.