GithubHelp home page GithubHelp logo

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

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;
}

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.

Double assertion

Input:

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

Output:

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

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

    // ...
},

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?

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) => {
     ....
}

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.

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.

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

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

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())
  }
  ...
}

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 => {

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;
}

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?

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.

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.

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

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

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

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.

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

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

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.

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.

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.

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

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}`;
}

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

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'

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

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.

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.