-
Notifications
You must be signed in to change notification settings - Fork 2
Project goals #19
Comments
Just pushed my very work in progress thing here in case you're interested - https://github.com/codemix/runtime-types |
Hi, Yes, the goal of this library is to be complementary to Flow, so it will be a small subset:
// example of type extraction
import * as t from 'flow-runtime'
import type { TypeOf } from 'flow-runtime'
const Person = t.object({
name: t.string,
age: t.number
})
// this is equivalent to
// type PersonT = { name: string, age: number };
type PersonT = TypeOf<typeof Person>; |
The ability to validate runtime declared types statically is very nice, but do you foresee people using the DSL directly or will you offer a babel plugin as with tcomb? My goals are very similar, but I want to achieve full compatibility with flow because then the system is capable of representing and validating any possible JS value. I'm about 85% done with my project but it would be nice if we could combine them or collaborate in some way because they're essentially the same thing to outside observers, would you consider expanding the scope to aim for full flow compat? |
If #15 will not be fixed, I think that the only solution in order to by DRY is to provide a babel plugin (but I'd prefer not to if possible).
This is interesting, what are the values that Flow is not able to validate so you need runtime type checking (beyond the IO boundary)? On the other hand static type checking is strictly more powerful than RTC for some tasks: checking function input / output and phantom types to name a few |
It's not that flow is lacking features, it's that I want to be able to use the type definitions that I'm writing anyway for other purposes - input validation, serialization, reflection etc and in order to do that I need to support everything flow supports (which isn't that much more than you already have here).
I do agree, but by the same token, runtime checking is strictly more powerful in other cases. They're complementary as you say. It's also very nice to be able to add constraints to types in a way that flow does not allow, e.g. you could define a type called The other major use case is when working with a codebase that is not all written with flow - every time control transfers into the legacy code you lose type safety, it's hard to verify that the type definitions you're writing for code that interacts with legacy are actually correct etc. If you can accurately assert types at runtime then that really makes it a lot easier to introduce flow to an existing code base. More than anything, I think it would be incredibly valuable to have a canonical way to represent types in JS at runtime, and to be canonical it has to be able to handle any kind of possible value. |
If you're interested in collaborating one way forward would be for me to port the babel plugin to target this library, at the moment it produces output that's very similar, right down to the lib name: type Demo = {
(foo: string): string;
(bar: boolean): boolean;
foo: string;
bar: number;
baz: number | string;
[key: string]: number;
[index: number]: boolean;
}; compiles to import t from "flow-runtime";
const Demo = t.type("Demo", t.object(
t.callProperty(t.function(
t.param("foo", t.string()),
t.return(t.string())
)),
t.callProperty(t.function(
t.param("bar", t.boolean()),
t.return(t.boolean())
)),
t.property("foo", t.string()),
t.property("bar", t.number()),
t.property("baz", t.union(t.number(), t.string())),
t.indexer("key", t.string(), t.number()),
t.indexer("index", t.number(), t.boolean())
)); |
I've always been obsessed by this idea, that's why I'm so interested in talking about this topic. I'm afraid that static types and runtime types are "different languages" though. Let me explain and show some problems I faced in the recent months (with
import type { SomeType } from 'external-library'
type MyType = {
some: SomeType
}; How can I convert
What if (...I'll think about it, and try to write down the other issues in the next few days) |
Also /cc @minedeljkovic as you are a "power user" of babel-plugin-tcomb:
|
I'm glad it's not just me, this is exciting!
Probably not in v0.0.1 but sure, in future that's a possible strategy. Another option would be to try and infer the type but you obviously have to be careful about not producing something too narrow. If you declare a function or class with type annotations then we could also associate the type with the value itself, e.g. function foo (): string { return "bar"; } could compile to (without checks): function foo () { return "bar"; }
foo[Symbol.typeDefinition] = t.function(t.return(t.string()); So we could also look for this symbol after importing. If we have no type information we'd just fall back to You are absolutely right that there will be gaps in such a system, but if flow can cover 99% of the cases statically, and we can cover 99% of cases at runtime then the odd cases where we have to allow |
The other option for the scenario you mentioned above is to just rely on flow itself when we don't know enough about an imported type. We can use |
Another option is to write a bunch of transformers to / from a JSON representation, but again there are many technical issues: after the experience with babel-plugin-tcomb I'm starting to think that an half backed solution is worse than no solution at all.
@phpnode this seems definitely out of scope of this library, when I say that they are complementary I think of the mathematical meaning: the intersection between runtime and static type checking should be as small as possible. There are 3 things Flow can't do
(*) actually you can do refinements with Flow, is just a bit awkward All those 3 things are currently covered by this lightweight library. The only problem I see is how to be DRY: you need a static type for Flow and a runtime type for IO validation, but in general I can't think of an automatic process that translates from one to another, besides the banal one type Person = {
name: string,
surname: ?string
}; to const Person = t.object({
name: t.string,
surname: t.maybe(t.string)
}) What if type Person<A> = {
name: string,
surname: A
}; Having kind function getPersonType(A: Type<*>): Type<*> {
return t.object({
name: t.string,
surname: A
})
} but before taking this route I'd love to see many examples in the wild, so I guess that the best thing to do now is to release a v0.1 and see what happens. I don't mean to bash your work with |
@gcanti I can't really see the difficulty with the example you provided - the lib already supports type parameters, e.g. const demo = <T> (input: T): T => input; Compiles to
This might not be the most beautiful output in the world but it can match flow semantics. The type parameter instance captures the type it first receives and uses it for all subsequent checks. I think I understand where you're coming from, I'm the author of babel-plugin-typecheck and I've run into many of the same headaches that you will have experienced with tcomb. It's why I'm so insistent that flow compatibility needs to be exact, because anything else makes the approach unusable. But it's definitely possible to achieve it, and I don't think doing so needs to bloat the library code itself (although obviously it would require some tooling around it) |
Yes and it's a clever transformation, but I wonder why I'd want to do that, when Flow works just fine and statically. Runtime type checking is a great tool when you can't adopt a static type checker or as a gentle introduction to Flow (the main goals of Let's put it in another way, I'm a "die-hard-static-types-guy" so the (admittedly opinionated) rationale behind this library is the following:
Currently 2) is the main (and only) goal of In addition, you may use the same combinators in order to get runtime type introspection (and runtime refinements). (*) perhaps I should rename my project |
It's true that runtime type checking is not particularly useful if your codebase is fully annotated, Flow has got so much better in the last 18 months. But if your codebase isn't totally annotated then it's still useful, as with However the primary motivation behind my library is very similar to yours - it's about being about to use the types for things that flow cannot do, not about replicating what flow already does. Really I just want types to be accessible at runtime, the fact that the types can be used to verify function args etc is almost irrelevant and I don't expect people will use that in production - for me that's more about verifying the completeness and compatibility of the system. I agree 100% with your goals, it's just that they're not my only goals - do a search for Edit: Here's the transformation for the actual example you posted, i.e. on a type rather than a function: type Person<A: string> = {
name: string;
surname: A;
}; -> import t from "flow-runtime";
const Person = t.type("Person", Person => { // Inside the function Person is PartialType<T>
const A = Person.typeParameter("A", t.string()); // declare our type parameter, in this case bound to string.
return t.object( // The return value completes the PartialType
t.property("name", t.string()), // We could also allow a simple map of property names to types here.
t.property("surname", A)
);
});
// Person is now TypeAlias<ObjectType<<A: string>() => {name: string, surname: A}>> |
Thanks for the example. And then, how do you use type Person<A> = {
name: string;
surname: A;
}; |
To reference import {reify} from 'flow-runtime';
type Person<A> = {
name: string;
surname: A;
};
const PersonType = (reify: Type<Person>); // reify is just a placeholder, has type any
PersonType.validate({name: 'bob', surname: false}); -> import t, {reify} from 'flow-runtime';
const Person = t.type('Person', Person => {
const A = Person.typeParameter('A');
return t.object(
t.property('name', t.string()),
t.property('surname', A)
);
});
const PersonType = Person;
PersonType.validate({name: 'bob', surname: false}); Which is a bit hacky but has the nice property that flow understands it. I might be missing something but why does Slightly related, how would you describe a self-referential type in your lib? e.g. type User = {
friends: User[];
} I use a similar mechanism as for type parameters - const User = t.type('User', User => t.object(t.property('friends', t.array(User))); |
The same result writing the definitions by hand (current implementation on master) import {
Type,
ObjectType
} from '../src/index'
import * as t from '../src/index'
type Person<A> = {
name: string,
surname: A
};
function getPersonType<T>(A: Type<T>): ObjectType<Person<T>> {
return t.object({
name: t.string,
surname: A
})
}
// monomorphization
const PersonType = getPersonType(t.string) // <= type-at-pos says PersonType is ObjectType<Person<string>>
const apiJSON = {name: 'bob', surname: false} // <= comes from a fetch
t.validate(apiJSON, PersonType)
Yep https://github.com/gcanti/babel-plugin-tcomb#runtime-type-introspection
Mmhh this is a slippery ground, Flow likely would augment the type with a union.
In a very similar way const User = t.recursion('User', User => t.object({ friends: t.array(User) })) |
Ah I see, I've been calling that a "type parameter application" but I probably have my terminology wrong. Right now type Person<A> = {
name: string,
surname: A
};
type PersonType = Person<string>; -> const Person = t.type("Person", Person => {
const A = Person.typeParameter("A");
return t.object(
t.property("name", t.string()),
t.property("surname", A)
);
});
const PersonType = t.type("PersonType", t.ref(Person, t.string())); The important bit here is
Wow, that is basically identical.
Doesn't it simply check that whenever a value is "written" to a particular type parameter, all writes to that instance must be (loosely) compatible? That's the semantics we have here. Maybe I'm misunderstanding. |
I was referring to the previous example where type T<A> = {
a: A,
b: A
};
declare function f<A>(x: T<A>): void;
f({ a: 1, b: 's' }) // <= A is number | string |
oh I see, by "loosely compatible" I meant it would consider the value |
@phpnode just renamed to
I'm going to release a v0.1 soon, with the only scope of IO validation written "by hand" (Flow types + runtime types). Then we can learn from real world use cases what can be possibly automated (or if we can join the forces on a common project). Your goal, while interesting, seems too broad for my current use case. Being a (semi)happy user of Flow, I only need to cover the remaining bits for now. Thanks for the interesting discussion, it helped me to focus on the very goal of this project. |
@gcanti understood, appreciate you changing the name and thanks for the enlightening discussion! I'll keep watching this project and let you know when flow-runtime is released, hopefully we'll be able to join the projects in some way in future - I'll certainly be copying some aspects of flow-io, especially the error reporting and static runtime type validation which are both really nice. All the best and thanks again. |
Thanks, I'll keep an eye on your project and feel free to reach out if you want to discuss some issues you encountered
👍 it's what open source is all about from my POV, cross pollination of (hopefully) good ideas |
Joining the conversation a bit late :) While I completely understand your goals gcanti (focus on the IO, the only problem you really have with Flow), I also agree with phpnode about having one way to represent types, accessible at runtime. It may allow something that is missing from all your (wonderful) tcomb toolkit : generative/property-based testing (à la QuickCheck). The Clojure community understand it very well. Since v1.9 Clojure includes a library similar to tcomb, but compatible with their core generative testing library. It makes expressing domain problems and testing them thoroughly extremely nice. But Clojure doesn't have static typing (typed clojure doesn't count). Javascript has a lib for generative testing, jsverify, but it has its own lonely way of expressing types. I'm not bashing it at all, just saying it lives in a silo when it comes to representing types in Javascript. You (gcanti) are bringing something else to the table with babel-plugin-tcomb and flow-io : bridge the gap between static and runtime check. If there could be a unified way to represent types at runtime, based on Flow syntax, we could use it to make generative testing in Javascript. Having this would put Javascript on top of the game : static typing + runtime typing + generative testing with the same syntax. When you include the rest of the tcomb toolkit (for docs, schema, etc.), one can truly achieve something unprecedented in current mainstream programming languages. As you say, one step at a time. It's been a long time I've wanted to tell you this. Flow is very good, tcomb is too. Javascript really needed them. It also needs a good generative testing library that interoperates with a type representation, and Flow's/yours with tcomb may just it. Thanks for reading. |
@DjebbZ Well there is, technically the purest form is a predicate const string: (value: mixed) => boolean
= value => typeof value === 'string' Things get more complicated when you want better error messages Validation function const string: (value: mixed) => Either<Error, string>
= value => ... and a name
interface Type<T> {
name: string,
validate: (value: mixed) => Either<Error, T>
} and runtime type introspection
interface ObjectType<T> {
name: string,
validate: (value: mixed) => Either<Error, T>,
props: { [key: string]: Type<any> };
} Having a real unified way means either
Related |
@DjebbZ I think that ultimately the single source of truth should be the Flow syntax and the types defined for Flow because
Once you have defined the types, you can generate many useful things through some transformations of the babylon AST
You could also generate the types from external sources, for example
The AST, or a simplified JSON DSL representing Flow types, may be, as a common language, the key to interoperability. This is an little experiment https://github.com/gcanti/flow-io/tree/transformers/src (see the and here some tests to get the general idea |
The issue with this is that it's very useful to be able to quickly tell whether a particular value is actually a value or a type, and that's hard to do safely with POJOs. Also, const string = {
name: 'string',
validate () { ... }
};
// string is just a value
const stringT = t.type(string);
// stringT is Type<typeof string>
stringT.assert('something'); |
I agree with @gcanti on this. The transformer examples are very thought provoking, but would be great to have more real world examples. Are they intended for production use? It seems like they rely on babylon runtime, which is generally not a part of frontend code... |
In case anyone's interested I published the first version of |
I like the idea of flow-runtime, but I just spent a couple of weeks
removing babel-plugin-tcomb from my apps due to subtle differences between
flow types and runtime types. So I'm wondering whether the same thing can
happen with flow-runtime as well?
…On Wed, Jan 4, 2017 at 3:51 PM Charles Pick ***@***.***> wrote:
In case anyone's interested I published the first version of flow-runtime
- https://codemix.github.io/flow-runtime
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#19 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAJQMFJhhQXEvY5MneKZdOfDWlTcf9JDks5rPAZNgaJpZM4Kvp4D>
.
|
@volkanunsal it can happen but it would be a bug so if you find such a case please file an issue, i'm striving for full flow compatibility |
@phpnode hi, I'm glad to hear that, I'd love to add a link to flow-runtime in the readme, would you like to send a PR? |
Hey, this is a cool project! I've been working on something really similar - I even called it
flow-runtime
until I saw you'd grabbed the name :). I was wondering what your goals for the project are? Do you intend to support all of flow's features such as type parameters, object indexers, functions etc or will you keep to a smaller subset? I think there's tremendous value in having a canonical way to represent types in JS, the ecosystem really needs a project like this.The text was updated successfully, but these errors were encountered: