gcanti / babel-plugin-tcomb Goto Github PK
View Code? Open in Web Editor NEWBabel plugin for static and runtime type checking using Flow and tcomb
License: MIT License
Babel plugin for static and runtime type checking using Flow and tcomb
License: MIT License
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
function head<A>(xs: Array<A>): ?A {
return xs[0]
}
throws A is not defined
Failing example:
const A = { type: 'Nil' }
type B = typeof A | string;
Along the lines of babel-plugin-flow-react-proptypes
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
ActionType
Example
function foo(a?: string) {
return a
}
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}`;
}
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> {
...
}
DB
represents a side effectcreateUser
is (username: string) => Eff<{ write: DB }, User>
. It means that createUser
writes to the DB and gives you a User
backlookupUser
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
:
write: 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.
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 GET
s 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
.
Eff
monadexport 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())
}
...
}
Input:
register(token: string) {
// ...
},
Output:
register: function register(token) {
_assert(token, _tcomb2.default.String, 'token');
_assert(token, _tcomb2.default.String, 'token');
// ...
},
Note. The term "FlowScript" here means JavaScript + Flow type annotations (+ PureScript idioms).
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
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 let
s are translated to const
s
export const example: Function3<number, number, number, number> =
x => y => z => {
const foo = x * y
const bar = y * z
return foo + bar
}
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 Maybe
s 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 :(
PureScript
data List a = Nil | Cons a (List a)
FlowScript
type List<A> = { type: 'Nil' } | { type: 'Cons', head: A, tail: List<A> };
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
}
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.
show
PureScript
show ( Just false ) -- "Just(false)"
FlowScript (a bit verbose...)
show(showMaybe(showBoolean))(Just(false)) // "Just(false)"
Hello,
do you think doing something like https://github.com/natefaubion/sparkler#how-does-it-work would be possible with babel-plugin-tcomb ?
Thanks for the hard work !
Causes an error:
const f = (y: t.String, { x }) : t.Object => ({ x, y });
Does not cause an error:
const f = (y: t.String, { x }) => ({ x, y });
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' repoI believe this would aid people in understanding the inner guts of how this works as well as the interop between flow and tcomb functionality.
Eg.
type MyString = string;
=> const MyString = t.String;
type MyRecord = {myString: string};
=> const MyRecord = t.interface({myString: t.String});
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) => {
....
}
Throws error
function foo(x = {} : t.Object) : t.Object {
return x;
}
Does not throw
function foo(x = {} : t.Object) {
return x;
}
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.
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.
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
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.
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.
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
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?
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
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) => ...
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 });
@gcanti Can you expand on the use case for the $Reify
type? I was looking around for an example on how to use this, but couldn't find one.
function(bar = 'foo) {
return bar;
}
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.
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.
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;
}
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.
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
Currently this fails:
type A = string | true;
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 }));
}
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 => {
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.
Failing test
function foo({ x }: { x: t.String }) {
return bar;
}
This example throws an error with the current implementation
function foo(x: t.Number = 0, y: t.String) {
return x + y;
}
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.
Also do you intend to parse union and intersection types?
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.
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.
I will use the following tools:
Flow
type annotations to corresponding tcomb
models and asserts.Runtime type checking (tcomb)
Flow
tcomb
's typesStatic 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.
You can add type safety to your untyped codebase gradually:
tcomb
Flow
and unleash the power of static type checkingFirst, 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.
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
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.
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
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 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>'
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.
Failing test case
function length(x: Array<*>) {
return x.length
}
If you define some interfaces:
export interface MyInterface {
...
}
eslint might raise an error:
MyInterface is not defined
This eslint plugin is helpful https://github.com/zertosh/eslint-plugin-flow-vars
Failing test
function bar({ a } = {}): t.String {
return x;
}
Currently only identifiers are supported, this throws:
const a = ('a value': A)
TypeError: Property value expected type of string but got null
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?
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'
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.