-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comparison with Facebook Flow Type System #1265
Comments
This is interesting and a good starting point for more discussion. Do you mind if I make some copyediting changes to the original post for clarity? |
Unexpected things in Flow (will be updating this comment as I investigate it more) Odd function argument type inference: /** Inference of argument typing doesn't seem
to continue structurally? **/
function fn1(x) { return x * 4; }
fn1('hi'); // Error, expected
fn1(42); // OK
function fn2(x) { return x.length * 4; }
fn2('hi'); // OK
fn2({length: 3}); // OK
fn2({length: 'foo'}); // No error (??)
fn2(42); // Causes error to be reported at function definition, not call (??) No type inference from object literals: var a = { x: 4, y: 2 };
// No error (??)
if(a.z.w) { } |
Feel free Like I said the purpose is to try investing flow type system to see if some features could fit in TypeScript one. |
@RyanCavanaugh I guess the last example : var a = { x: 4, y: 2 };
// No error (??)
if(a.z.w) { } Is a bug a related to their null check algorithm, I'll report it. |
Is type A = () => void & (t: string) => void
var func : A; Equivalent to Declare A : () => void | (t: string) => void
var func : A; Or could it be? |
@Davidhanson90 not really : declare var func: ((t: number) => void) | ((t: string) => void)
func(3); //error
func('hello'); //error in this example flow is unable to know which type in the union type declare var func: ((t: number) => void) & ((t: string) => void)
func(3); //no error
func('hello'); //no error func has both type so both call are valid. |
Is there any observable difference between |
@RyanCavanaugh I don't really know after thought I think it's pretty much the same still thinking about it. |
|
Another difference is that Flow propagates type inference backwards to broaden the inferred type into a type union. This example is from facebook/flow#67 (comment) class A { x: string; }
class B extends A { y: number; }
class C extends A { z: number; }
function foo() {
var a = new B();
if (true) a = new C(); // TypeScript reports an error, because a's type is already too narrow
a.x; // Flow reports no error, because a's type is correctly inferred to be B | C
} ("correctly" is from the original post.) This means the following will compile too. var x = "5"; // x is inferred as string
if ( true) { x = 5; } // Inferred type is updated to string | number
x.toString(); // Compiles
x += 5; // Compiles. Addition is defined for both string and number after all, although the result is very different Edit: Tested the second snippet and it really does compile. |
Do you like this behavior? We went down this route when we discussed union types and the value is dubious. Just because the type system now has the ability to model types with multiple possible states doesn't mean it's desirable to use those everywhere. Ostensibly you have chosen to use a language with a static type checker because you desire compiler errors when you make mistakes, not just because you like writing type annotations ;) That is to say, most languages give an error in an example like this (particularly the second one) not for lack of a way to model the type space but because they actually believe this is a coding error (for similar reasons many eschew supporting lots of implicit cast/conversion operations). By the same logic I would expect this behavior: declare function foo<T>(x:T, y: T): T;
var r = foo(1, "a"); // no problem, T is just string|number but I really do not want that behavior. |
@danquirk I agree with you that inferring union type automatically instead of reporting an error is not a behavior that I like. |
The exact strictness is even debatable given the knock on effects of this sort of behavior. Often it's just postponing an error (or hiding one entirely). Our old type inference rules for type arguments very much reflected a similar philosophy. When in doubt, we inferred {} for a type parameter rather than make it an error. This meant you could do some goofy things and still do some minimal set of behaviors safely on the result (namely things like declare function foo(arg: number);
var x = "5";
x = 5;
foo(x); // error What's the error here? Is it really passing |
Arguably leaving |
A not-insignificant part of Flow's marketing is based around the fact that their typechecker makes more sense of code in places where TS would infer It comes down to whether someone has the expectation that
TS currently does (1) and Flow does (3). I prefer (1) and (2) much more over (3). |
I wanted to add @Arnavion examples to the original issue but after playing a bit I realized that things where stranger than what we understood.
Now :
after this example x is
I got an error at line 1 saying : I still need to figure the logic here .... |
There is also an interesting point about flow that I forgot : function test(t: Object) { }
test('string'); //error Basically 'Object' is non compatible with other primitive type, I think that one make sense. |
The 'Generic resolution capture' is definitely must-have feature for TS! |
@fdecampredon Yes you're right. With The error you get I've updated the example in my comment to have the |
+1 |
@Arnavion, not sure why you would prefer The example
already typechecks under the current compiler, except T is inferred to be Infering |
I think that allowing the + operation on something that can be either a string or a number is compromising correctness, since the operations are not similar to each other at all. It's not like the situation where the operation belongs on a common base class (my option 2) - in that case you can expect some similarity. |
The + operator would not be callable, as it would have two incompatible overloads - one where both arguments are numbers, and one where both are strings. Since B | C is narrower than both string and number, it would not be allowed as an argument in either overload. Except functions are bivariant wrt their arguments so that might be a problem? |
I thought that since
|
A lot of the comments have focused on the automatic broadening of types in Flow. In both cases you can have the behavior you want by adding an annotation. In TS you would broaden explicitely at declaration: As for the Flow features that I think are the most valuable?
|
On the automatic union type inference: I assume "type inference" to be restricted to the type declaration. A mechanism that implicitly infers an omitted type declaration. Like I understand the argument about letting real-life JS use dictate TS semantics, but this is perhaps a good point to follow other languages on, instead. Types are the one single defining difference between JS and TS, after all. I like a lot of the Flux ideas, but this one, well, if it's actually done this way... that's just weird. |
Non-null types seem like a mandatory feature for a modern type system. Would it be easy to add to ts? |
If you want some light reading on the complexities of adding non-nullable types to TS see #185 Suffice to say, as nice as non-nullable types are the vast majority of popular languages today do not have non-nullable types by default (which is where the feature truly shines) or any generalized non-nullability feature at all. And few, if any, have attempted to add it (or successfully added it) after the fact due to the complexity and the fact that so much of the value of non-nullability lies in it being the default (similar to immutability). This is not to say we aren't considering the possibilities here, but I wouldn't call it a mandatory feature either. |
Actually as much as I miss non-null type, the real feature I miss from flow are generic capture, the fact that ts resolve every generic to |
Personally, generic capture and non-nullability are high value targets from Flow. I'll read the other thread, but I wanted to throw my 2c in here as well. I sometimes feel that the benefit of adding non-nullability is worth nearly any cost. It is such a high likelihood error condition and while having default nullability weakens the built-in value right now TypeScript lacks the ability to even discuss nullability by simply assuming it's the case everywhere. I would annotate every variable I could find as non-nullable in a heartbeat. |
There are quite a lot hidden features in flow, not documented in flow's site. Including SuperType bound and existential type |
Also I won't speak about missing features in Flow since the purpose is as stated to improve TypeScript.
Finally this topic is only about type system and not about supported es6/es7 features.
mixed
andany
From the flow doc :
Basically that's mean that with flow
any
is the equivalent of TypeScriptany
andmixed
is the equivalent of TypeScript{}
.The
Object
type with flowFrom flow doc :
With TypeScript
Object
is the equivalent of{}
and accept any type, with FlowObject
is the equivalent of{}
but is different thanmixed
, it will only accepts Object (and not other primitive types likestring
,number
,boolean
, orfunction
).In this example the parameter of
logObjectKeys
is tagged with typeObject
, for TypeScript that is the equivalent of{}
and so it will accept any type, like anumber
in the case of the second calllogObjectKeys(3)
.With Flow other primitive types are not compatible with
Object
and so the type-checker will report and error with the second calllogObjectKeys(3)
: number is incompatible with Object.Type are non-null
From flow doc :
see Flow doc section
Since the flow doc is pretty complete I won't describe this feature in details, just keep in mind that it's forcing developer to have every variables to be initialized, or marked as nullable, examples :
However like for TypeScript type guard feature, flow understand non-null check:
Intersection Type
see Flow doc section
see Correspondin TypeScript issue #1256
Like TypeScript flow support union types, it also support a new way of combining types : Intersection Types.
With object, intersection types is like declaring a mixins :
AB has for type
{ foo: string; bar : string;}
;For functions it is equivalent of declaring overload :
is equivalent to :
Generic resolution capture
Consider the following TypeScript example:
With TypeScript
promisifiedIdentity
will have for type:With flow
promisifiedIdentity
will have for type:Type inference
Flow in general try to infer more type than TypeScript.
Inference of parameters
Let's give a look at this example :
With TypeScript, no errors are reported, with flow the last call of
logLength
will result in an error becausenumber
does not have alength
property.Inferred type changes with usage
With flow unless you expressly type your variable, the type of this variable will change with the usage of this variable :
In this example x has initially
string
type, but when assigned to a number the type has been changed tonumber
.With typescript the assignation
x = 5
would result in an error sincex
was previously assigned tostring
and its type cannot change.Inference of Union types
Another difference is that Flow propagates type inference backwards to broaden the inferred type into a type union. This example is from facebook/flow#67 (comment)
("correctly" is from the original post.)
Since flow detected that the
a
variable could haveB
type orC
type depending of a conditional statement, it is now inferred toB | C
, and so the statementa.x
does not result in an error since both types has anx
property, if we would have tried to access thez
property and error would have been raised.This means the following will compile too.
Edit
mixed
andany
section, sincemixed
is the equivalent of{}
there is no need for example.Object
type.Feel free to notify if I forgot something I'll try to update the issue.
The text was updated successfully, but these errors were encountered: