Comments (42)
Just pushed a candidate implementation (flow
branch).
Despite being black magic, I'm really tempted because it's working extraordinarily well.
For example, adding the following simple file definition:
declare module 'tcomb' {
declare interface Type<T> {
(x: T): T;
is(x: any): boolean;
}
declare interface $Refinement<P: (x: any) => boolean> {}
declare var Integer: Type<number>;
}
I'm now able to do this:
import type { $Refinement } from 'tcomb'
import { Integer } from 'tcomb'
const p = n => n >= 2
type IntegerT = number & $Refinement<typeof Integer.is>;
type IntegerGreaterThan2 = IntegerT & $Refinement<typeof p>;
function foo(n: IntegerGreaterThan2) {
return n
}
foo(2) // flow ok, tcomb ok
foo(1) // flow ok, tcomb throws [tcomb] Invalid value 1 supplied to n: IntegerGreaterThan2
foo(2.1) // flow ok, tcomb throws [tcomb] Invalid value 2.1 supplied to n: IntegerGreaterThan2
foo('a') // flow throws, tcomb throws
from babel-plugin-tcomb.
Another proposal, validating (at runtime) the IO boundary using typecasts:
type User = { name: string };
export function getUser(userId: string): Promise<User> {
return axios.get('...').then(p => (p: User)) // <= type cast
}
to
const User = t.interface({
name: t.String
}, "User");
export function getUser(userId: string): Promise<User> {
_assert(userId, t.String, "userId");
const ret = function (userId) {
return axios.get('...').then(p => {
return _assert(p, User, "p"); // <= runtime validation
});
}.call(this, userId);
_assert(ret, Promise, "return value");
return ret;
}
from babel-plugin-tcomb.
why is this the case?
AFAIK because interface
can describe almost anything and Flow doesn't assume it describes objects, so:
interface $Refinement<P: (x: any) => boolean> {}
is equivalent to type Refinement<P: (x: any) => boolean> = any;
or simply any
so
type T1 = number & $Refinement<typeof isPositive>;
// equivalent to
type T2 = number & any;
// equivalent to
type T3 = number;
same here, what's the ,?
an interface can extend n
other interfaces
from babel-plugin-tcomb.
I mean, how to combine static type checking with runtime checking without boilerplate code:
tcomb repeats part of flow functionality only for runtime and in own syntax: struct, func, etc.
All things, that can be expressed via flow, must be expressed via flow.
/* @flow */
import {
String as string,
struct
} from 'tcomb'
const MyObj = struct({
t: string
})
type MyObj = {
t: string;
}
export function test(arg: MyObj): MyObj {
return arg
}
test({
t: 123 // flow generates error
})
Just transpile
type MyObj = {
t: string;
}
into
const MyObj = struct({
t: string
})
from babel-plugin-tcomb.
Hi @zerkalica,
working on this (again...hope I have some good news though).
I'm compiling:
string
tot.String
number
tot.Number
boolean
,bool
tot.Boolean
any
,mixed
tot.Any
void
,null
tot.Nil
It's not required to do the trick you suggested:
import {
String as string // trick
} from 'tcomb'
Example
import t from 'tcomb';
function foo(x: string) {
return x
}
now compiles to:
import t from 'tcomb';
function foo(x: string) {
t.assert(t.is(x, t.String), 'Invalid argument x (expected a ' + t.getTypeName(t.String) + ')');
return x;
}
Note the new t.is
call, here's its code in tcomb
:
var isType = require('./isType');
// returns true if x is an instance of type
module.exports = function is(x, type) {
if (isType(type)) {
return type.is(x);
}
return x instanceof type; // type should be a class constructor
};
The x instanceof type
part will allow to handle non-tcomb types, generics included:
import t from 'tcomb';
function foo(p: Promise<string>): void {}
foo(1) // throws [tcomb] Invalid argument p (expected a Promise)
at the same time Flow throws:
index.js:9
9: foo(1)
^ number. This type is incompatible with
5: function foo(p: Promise): void {
^^^^^^^^^^^^^^^ Promise
Now, coming to your last example, this works:
import t from 'tcomb'
const MyObj = t.interface({
t: t.String
}, 'MyObj')
export function test(arg: MyObj): MyObj {
return arg
}
test({
t: 123 // throws [tcomb] Invalid argument arg (expected a MyObj)...
})
// ...while Flow says no errors!
but this doesn't:
/* @flow */
type MyObj = {
t: string
};
export function test(arg: MyObj): MyObj {
return arg
}
test({
t: 123 // here Flow correctly throws "number. This type is incompatible with string"
})
because JavaScript throws "ReferenceError: MyObj is not defined".
Now we could check that MyObj
is defined, i.e. something like:
function test(arg: MyObj): MyObj {
MyObj = typeof MyObj !== 'undefined' ? MyObj : t.Any;
t.assert(t.is(arg, MyObj), 'Invalid argument arg (expected a ' + t.getTypeName(MyObj) + ')');
...
}
or we could compile:
type MyObj = {
t: string
};
to:
const MyObj = t.interface({
t: t.String
})
Not sure what's better, what do you think?
/cc @ctrlplusb @chrisui @ivan-kleshnin @majhork
from babel-plugin-tcomb.
Just added (basic) support for interfaces:
interface A {
a: string;
}
interface B extends A {
b: string;
}
to
const A = t.interface({
a: t.String
}, "A");
const B = t.interface.extend([A, {
b: t.String
}], "B");
from babel-plugin-tcomb.
@gabro it doesn't work with version 0.26.0 :(
This works though (and being an expression seems more versatile):
import type { $Reify } from 'tcomb'
type Person = {
name: string
};
const X = (({}: any): $Reify<Person>)
X.meta // OK
if (X.meta.kind === 'interface') {
X.meta.props // OK
}
tested against the following incomplete (WIP) definition file:
declare module 'tcomb' {
// runtime type introspection
declare type $Reify<T> = Type<T>;
declare type MetaIrreducible = {
kind: 'irreducible'
};
declare type MetaList = {
kind: 'list',
type: Type
};
declare type MetaInterface = {
kind: 'interface',
props: {[key: string]: Type}
};
// TODO(giu) describe all meta objects
declare type Meta =
MetaIrreducible
| { kind: 'refinement' }
| { kind: 'maybe' }
| { kind: 'struct' }
| MetaInterface
| { kind: 'func' }
| { kind: 'tuple' }
| MetaList
| { kind: 'dict' }
| { kind: 'enums' }
| { kind: 'union' }
| { kind: 'intersection' };
declare interface $Refinement<P: (x: any) => boolean> {}
declare interface Type<T> {
(x: T): T;
is(x: any): boolean;
meta: Meta;
}
declare class Tcomb {
// utils
assert(guard: boolean, message?: string | () => string): void;
stringify(x: any): string;
// irreducibles
Nil: Type<void | null>;
String: Type<string>;
Number: Type<number>;
Boolean: Type<boolean>;
Integer: Type<number>;
Function: Type<Function>;
Array: Type<Array<any>>;
// combinators
list<T>(type: Type<T>, name?: string): Type<Array<T>>;
}
declare var exports: Tcomb;
}
from babel-plugin-tcomb.
@gcanti - installing this tomorrow. Shall report back with any issues. Pretty exciting stuff. I'm gonna experiment with the static checking too.
from babel-plugin-tcomb.
Hi @zerkalica,
Since my data structures and domain models are already implemented with tcomb
I just needed a quick way to type-check functions using tcomb
types without using t.func
combinator (which I find kind of annoying).
It's so difficult to create gracefully degraded to flow abstraction over tcomb?
Not sure what you mean, Could you elaborate a little bit? What's your use case?
from babel-plugin-tcomb.
This is pretty epic @gcanti. Fantastic as always.
My vote is for the transpilation of type MyObj
to const MyObject = t.interface
. Certainly more work but I feel like it would be fantastic for tcomb
if you could support as much of the flow syntax as possible. Firstly, it could lead to greater tcomb
adoption, granting a migration path for existing flow
users. Additionally this library is already piggy backing off the babel flow syntax plugin, so I believe it will make things feel much more natural if we move to support the "native" flow syntax - perhaps this will save us headache or two in the future, or provide us with a great base from which to adapt against flow syntax changes/extensions.
from babel-plugin-tcomb.
Ok we got type aliases
type Person = {
name: string,
surname?: string,
age: number,
tags: Array<string>
};
function getFullName(p: Person) {
return `${p.name} ${p.surname}`
}
getFullName({})
tcomb (at runtime)
throws [tcomb] Invalid argument p (expected a Person)
Flow
> flow
index.js:14
14: getFullName({})
^^^^^^^^^^^^^^^ function call
10: function getFullName(p: Person) {
^^^^^^ property `age`. Property not found in
14: getFullName({})
^^ object literal
index.js:14
14: getFullName({})
^^^^^^^^^^^^^^^ function call
10: function getFullName(p: Person) {
^^^^^^ property `name`. Property not found in
14: getFullName({})
^^ object literal
index.js:14
14: getFullName({})
^^^^^^^^^^^^^^^ function call
10: function getFullName(p: Person) {
^^^^^^ property `tags`. Property not found in
14: getFullName({})
^^ object literal
Found 3 errors
The problem now is: I don't know how to define refinements without irritate Flow:
/* @flow */
import t from 'tcomb'
type Person = {
name: string,
surname?: string,
age: number,
tags: Array<string>
};
const PersonRefinement = t.refinement(Person, p => p.surname === 'aaa')
> flow
index.js:12
12: const PersonRefinement = t.refinement(Person, p => p.surname === 'aaa')
^^^^^^ Person. type referenced from value position
5: type Person = {
^^^^^^ type Person
Found 1 error
from babel-plugin-tcomb.
First of all create normal reflection layer for each type, constructor, method and function.
There are many realisations: babel-plugin-type-metadata, babel-plugin-angular2-annotations, or my babel-plugin-transform-metadata
// interfaces.js
// @flow
export type User = {
name: string;
}
to:
// interfaces.js
// @flow
import t from 'tcomb'
export type User = {
name: string;
}
export tcomb$User = t.interface({
name: t.string
})
Interface usage:
// @flow
import type {User} from './interfaces'
function test(user: User): void {}
to
// @flow
import type {User} from './interfaces'
import {tcomb$User} from './interfaces'
import t from 'tcomb'
const metadata$test = [tcomb$User]
function test(user: User): void {
t.assert(metadata$test)
}
Reflect.defineMetadata('design:paramtypes', metadata$test, test)
PS:
I think, it's too hard to create real flow-compatible runtime checking. Look at https://github.com/codemix/babel-plugin-typecheck, it supports generics, imports/exports, optimizations. Code base about 3000 lines, but stil not flow-compatible.
from babel-plugin-tcomb.
it's too hard to create real flow-compatible runtime checking
Yes I agree, but my goal is different, for instance I don't want to re-implement at runtime what can be done only by Flow, I think it's not even possible.
Static type checking and runtime type checking are complementary:
- static type checking: typos, generics, refactoring, cross file type errors (i.e. you change a LOC in a file and you break the code of another part of the app)
- runtime type checking: refinements, runtime type introspection, IO validation, use cases when you can't setup Flow
First of all create normal reflection layer for each type, constructor, method and function
Not sure I'm following, why you'd do that? The goal is to type-check, not to feed the reflection API (which is still just a proposal at this time)
from babel-plugin-tcomb.
Reflect polyfil just an example. Main idea - generate and share tcomb metada for different puproses
and access them in runtime.
For example, for form validation: https://github.com/seanhess/runtime-types
from babel-plugin-tcomb.
Main idea - generate and share tcomb metada for different puproses and access them in runtime.
Ending with parallel type definions... How are you going to track and solve unsync issues?
from babel-plugin-tcomb.
@gcanti just to understand the refinement problem a bit better, even if you disambiguate the usage of a type as a value, how are you going to use the refinement anyway?
Basically, this:
import t from 'tcomb'
type Person = {
name: string,
surname?: string,
age: number,
tags: Array<string>
};
const PersonRefinement = t.refinement(Person, p => p.surname === 'aaa')
// ^___ suppose this works, somehow
function getSurname(p: PersonRefinement) { // how does flow react to this?
return p.surname;
}
from babel-plugin-tcomb.
@ivan-kleshnin What you mean, what is unsync issues?
from babel-plugin-tcomb.
how does flow react to this?
@gabro After a few hours of trials I'd say... "badly". You can't use a value as a type annotation (at least I didn't find a way).
There's a divide between the world of types and the world of values so it seems you must choose: either you give up on refinements and RTI:
// @flow
type Person = {
name: string
};
function foo(p: Person) {
return p.name
}
foo({}) // flow throws property `name`. Property not found in
// tcomb throws [tcomb] Invalid argument p (expected a Person)
or you give up on static type checking and go dynamic (you gain refinements and RTI):
import t from 'tcomb'
const Person = t.interface({
name: t.String
})
function foo(p: Person) {
return p.name
}
foo(Person({})) // tcomb throws Invalid value undefined supplied to {name: String}/name: String
// no errors for Flow
There is a slightly verbose middle ground to explore though (static + dynamic):
// @flow
import t from 'tcomb'
type Person = {
name: string
};
const PersonRefinement = t.refinement(t.Object, p => p.name.length < 3)
function foo(p: Person) {
t.assert(PersonRefinement.is(p), 'bad!');
return p.name
}
foo({ name: 'Giulio' }) // no errors for flow but tcomb throws [tcomb] bad!
from babel-plugin-tcomb.
Isn't there a way of making a pass before flow does?
I think if we can have some kind of pipeline
pre-flow → flow → babel-plugin
we might do something interesting
from babel-plugin-tcomb.
I'm hesitant to go that route. I don't exclude a pre-processing step but it's potentially cumbersome, the nice thing about Flow is that you throw at it your source files and you are done.
If this thing works well, my preference at the moment is going completely static except for some strategic domain models which I may want to define as refinements.
from babel-plugin-tcomb.
A "real world" example: type-checking redux (state and actions) gcanti/redux-tcomb#9 (comment)
from babel-plugin-tcomb.
Added a _assert
helper that gives better error messages:
import t from 'tcomb'
const MyObj = t.interface({
t: t.String
}, 'MyObj')
export function test(arg: MyObj): MyObj {
return arg
}
// before: [tcomb] Invalid argument arg (expected a MyObj)...
// now: [tcomb] Invalid value 123 supplied to arg: MyObj/t: String
test({
t: 123
})
Source:
import t from 'tcomb'
const MyObj = t.interface({
t: t.String
}, 'MyObj')
export function test(arg: MyObj): MyObj {
return arg
}
Output:
import t from 'tcomb'
function _assert(x, type, name) {
...
}
const MyObj = t.interface({
t: t.String
}, 'MyObj')
export function test(arg: MyObj): MyObj {
_assert(arg, MyObj, 'arg');
var ret = function (arg) {
return arg;
}.call(this, arg);
_assert(ret, MyObj, 'return value');
return ret;
}
from babel-plugin-tcomb.
I have a proposal in order to support refinements, but it's kinda hacky, I'd love to hear what you think:
Let's define a special interface $Refinement
:
interface $Refinement<P: (x: any) => boolean> {}
$Refinement
is a "placeholder" and won't be compiled by the babel plugin.
Usage
interface $Refinement<P: (x: any) => boolean> {}
const isPositive = x => x >= 0
// "typeof isInteger" will allow the babel plugin to compile the actual refinement
// picking the "isPositive" identifier while Flow sees only the type: number
type Positive = number & $Refinement<typeof isPositive>;
function foo(x: Positive) {}
foo(-1) // ok for Flow (obviously it can't type-check refinements)
foo('a')
/*
index.js:14
14: foo('a')
^^^^^^^^ function call
14: foo('a')
^^^ string. This type is incompatible with
10: type Positive = number & $Refinement<typeof isPositive>;
^^^^^^ number
*/
compiles to:
const isPositive = x => x >= 0
// here the babel plugin does its magic
const Positive = t.refinement(t.Number, isPositive, 'Positive');
function foo(x: Positive) {
_assert(x, Positive, 'x')
...
}
// tcomb can type-check refinements
foo(-1) // throws [tcomb] Invalid value -1 supplied to x: Positive
foo('a') // throws [tcomb] Invalid value "a" supplied to x: Positive
Same for interfaces:
interface A {
name: string;
}
const predicate = (x: A) => x.name.length > 3
interface B extends A, $Refinement<typeof predicate> {}
function foo(b: B) {}
foo({name: 1})
compiles to:
const A = t.interface({
name: t.String
}, 'A')
const predicate = (x: A) => x.name.length > 3
const B = t.interface.extend([A, t.refinement(t.interface({
}), predicate)], 'B');
/cc @giogonzo
from babel-plugin-tcomb.
what kind of ancient sorcery am I looking at? jk, looks awesome 😀
type Positive = number & Refinement<typeof isPositive>
picking the "isPositive" identifier while Flow sees only the type: number
why is this the case?
interface B extends A, Refinement<typeof predicate> {}
same here, what's the ,
?
Apart from some context I'm missing on flow type annotations syntax, yes, this looks kind of hacky / burden on the end user. But yeah! for profit
from babel-plugin-tcomb.
This is great! I think the opportunities this solution opens greatly outweigh the "hackish feel". Well done! 👍
from babel-plugin-tcomb.
Last topic: anyone has an idea about getting runtime type introspection?
type Person = {
name: string
};
// this is ok in the runtime world
console.log(Person.meta) // => {kind: 'interface', props: ...}
but in the static world Flow throws:
index.js:9
9: console.log(Person.meta)
^^^^^^ Person. type referenced from value position
5: type Person = {
^^^^^^ type Person
/cc @samwgoldman
from babel-plugin-tcomb.
// @flow
import {_, getIntrospection} from 'tcomb'
type Person = {name: string}
console.log(getIntrospection((_: Person)))
_ is a dummy var with any type.
babel converts (_: Person) to Person.meta
from babel-plugin-tcomb.
As discussed offline, if we make an assignment mandatory, this should be possible:
import type reify from 'tcomb';
declare interface Person {
name: string
}
let Person: reify<Person>;
console.log(Person.meta);
The interesting bit is that we use a type for driving the transformation, so there's 0 risk of messing up with user's values.
from babel-plugin-tcomb.
Two errors:
Name is already bound and Property cannot be accessed on possibly undefined value
May be try this?
console.log((_: reify<IPerson>).meta);
from babel-plugin-tcomb.
Apparenty:
- types and values live in the same namespace (my bad, didn't know that)
- initialization (even to
null
) is required to silence thepossibly undefined value
warning
This should work
type reify<T> = any;
declare interface Person {
name: string
}
let tPerson: reify<Person> = null;
console.log(tPerson.meta);
from babel-plugin-tcomb.
Slightly different syntax, but essentially the same thing
import { reify } from 'tcomb';
import type type from 'tcomb;
declare interface Person {
name: string
}
let tPerson: type<Person> = reify; /// void
console.log(tPerson.meta);
where
export type type<T> = any;
export const reify = null;
No changes introduced, other than explicitly providing a value (reify
) to use on the rhs.
from babel-plugin-tcomb.
Closing this as per cea8fc1.
from babel-plugin-tcomb.
@ctrlplusb I'm commiting lib
on master so you can install the plugin simply running npm i gcanti/babel-plugin-tcomb#master
, could you please give it a whirl? I'd love to hear if there are regressions with respect to v0.2
from babel-plugin-tcomb.
@gcanti How do you plain to track situations with metadata, when flow definition placed in external library?
// node_modules/myLib/flow-typed/interface.js
declare module 'myLib' {
declare interface User {
name: string;
}
}
// app.js
import type {User} from 'myLib'
function (user: User) {
// ...
}
from babel-plugin-tcomb.
@zerkalica currently fallbacks to t.Any
:
import type { User } from 'myLib'
function (user: User) {
}
compiles to
var myLib = require('myLib');
function f(user) {
_assert(user, typeof myLib.User !== "undefined" ? myLib.User : t.Any, 'user');
console.log(user);
}
What do you suggest? (perhaps it's better to open a new issue though)
from babel-plugin-tcomb.
Hey @ctrlplusb how is going? The first impact with Flow can be overwhelming. I'd love to put up a babel-plugin-tcomb boilerplate showing what you can get in terms of type safety, maybe with redux / react / etc.. Or perhaps even better, pick an existing boilerplate and add type annotations. Any ideas?
from babel-plugin-tcomb.
Not him, but I tried yesterday. There were some issues. The native flow
types weren't recognized and I had to create aliases for them. Maybe
because I didn't use the '@flow' comment at top of file? I will try again
today with flow enabled.
On Thursday, June 16, 2016, Giulio Canti [email protected] wrote:
Hey @ctrlplusb https://github.com/ctrlplusb how is going? The first
impact with Flow can be overwhelming. I'd love to put up a
babel-plugin-tcomb boilerplate showing what you can get in term of type
safety, maybe with redux / react / etc.. Or perhaps even better, pick an
existing boilerplate and add type annotations. Any ideas?—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
#11 (comment),
or mute the thread
https://github.com/notifications/unsubscribe/AAJQMHSJq7DbSs0U6hOaNJ_Pd_gFdTRoks5qMPIzgaJpZM4H23ne
.
Volkan Unsal
web and mobile development
volkanunsal.com
from babel-plugin-tcomb.
Just pushed babel-plugin-tcomb-boilerplate, It's just the usual counter for now (I'll add other features step by step) but already shows the great benefits of having both static and runtime type checking. Feeding Flow is a little bit verbose but it's awesome having the whole app type checked.
from babel-plugin-tcomb.
@volkanunsal yesterday I fixed (73a2104) a major problem with types imported from definition files (they were undefined
).
The native flow types weren't recognized
Feel free to open an issue with some excerpt of your code, so we can hopefully come up with a solution and/or enhance the test suite.
from babel-plugin-tcomb.
Elm architecture with Flow and babel-plugin-tcomb: https://github.com/gcanti/babel-plugin-tcomb-boilerplate/tree/elm/src
from babel-plugin-tcomb.
Elm architecture with Flow and babel-plugin-tcomb: https://github.com/gcanti/babel-plugin-tcomb-boilerplate/tree/elm/src
@gcanti This is funny, I was also playing for the last week or so with Elm architecture in Redux, statically type checked with flow. I just pushed it to github today:
https://github.com/minedeljkovic/redux-elmish
I'd love to hear someone's comment or suggestion on this.
I am also very interested to use this repo to produce tcomb types for runtime introspection. For now I think of decoding part of model with fromJSON and tagged actions pretty print in redux devtools (with the help of tcomb union).
from babel-plugin-tcomb.
@minedeljkovic Really interesting and a great resource, thanks for sharing
from babel-plugin-tcomb.
Related Issues (20)
- Refinements example HOT 6
- Typechecking flow's inferred types
- Generator types
- Why generics are not supported
- arguments is not carried across during return type check
- Redux actions with constant string values
- Checking return value of generator function breaks the code HOT 2
- Working with Meteor's new reify HOT 1
- "ReactClass is not defined" error HOT 2
- Cannot set up for ReactNative project HOT 2
- support for argument spread
- add meta object for class?
- Add support for object type spread HOT 11
- tcomb crashes when there are circular "import type" HOT 4
- inspecting function argument and return types
- Adding Custom Error Message to Flow-typed Refinement?
- babel 7 support HOT 3
- ReferenceError: $Values is not defined
- The $Call-way
- React Native: _t is not defined
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from babel-plugin-tcomb.